# Partner - Store secure encrypted data
Source: https://developer.coincover.com/api-reference/backups/partner--store-secure-encrypted-data
/specs/wallet-provider.openapi.json post /v1/partner/secure/data
Stores base64-encoded encrypted data with checksum validation and padding type specification. The data is verified using the enclave before storage. Partner-only endpoint. The checksum is required and must be the SHA-256 of the plaintext.
# Partner - Store secure encrypted files
Source: https://developer.coincover.com/api-reference/backups/partner--store-secure-encrypted-files
/specs/wallet-provider.openapi.json post /v1/partner/secure/file
Stores encrypted files securely. The key environment is determined from the provided key. The file must already be encrypted on the partner side; CoinCover applies no additional server-side encryption. Maximum payload 10MB. Partner-only endpoint.
# Recover encrypted backup data
Source: https://developer.coincover.com/api-reference/backups/recover-encrypted-backup-data
/specs/retail.openapi.json post /v1/backup/recover
Recovers previously stored backup data. Requires successful identity verification (verification_id must be approved/successful). Requires a GPG public key for encrypting the recovered data. Returns an array of backup recovery results, each containing recovery information or error details. This endpoint calls the hot-keys orchestrator recover-backup endpoint for each backup associated with the user.
# Store encrypted backup data
Source: https://developer.coincover.com/api-reference/backups/store-encrypted-backup-data
/specs/retail.openapi.json post /v1/backup/store
Stores encrypted backup data securely. Accepts multiple backup items in a single request via the backup array (limited to a maximum of 3 items). Requires a verification_id that exists in the identity_verifications table and belongs to the user associated with the organisation. Each backup item's item_key (e.g., "seed_phrase", "private_key") is stored in the hot-keys database metadata as backup_item_key for recovery operations. This endpoint calls the hot-keys orchestrator partner-secure-data endpoint.
# Store secure encrypted data
Source: https://developer.coincover.com/api-reference/backups/store-secure-encrypted-data
/specs/institutional.openapi.json post /v1/secure/data
Stores base64-encoded encrypted data with optional checksum validation and padding type specification. The data is verified using the enclave before storage. Supplying the SHA-256 of the plaintext as the checksum is recommended.
# Store secure encrypted files
Source: https://developer.coincover.com/api-reference/backups/store-secure-encrypted-files
/specs/institutional.openapi.json post /v1/secure/file
Stores an already-encrypted file. CoinCover applies no additional server-side encryption.
# Verify a backup's integrity
Source: https://developer.coincover.com/api-reference/backups/verify-a-backups-integrity
/specs/institutional.openapi.json get /v1/backups/{backupId}/verify
Verifies that a backup is valid by checking the encrypted data.
# Assign a new key
Source: https://developer.coincover.com/api-reference/keys/assign-a-new-key
/specs/institutional.openapi.json post /v1/keys
Creates a new key pair with the enclave, shards the private key, and securely stores the encrypted key shards. organisation and package are optional; key_environment defaults to HOT if omitted.
# Generate a cryptographic key
Source: https://developer.coincover.com/api-reference/keys/generate-a-cryptographic-key
/specs/retail.openapi.json post /v1/key/generate
Generates a new cryptographic key for a user. Requires a user identifier and a verification_id that exists in the identity_verifications table and belongs to the user associated with the organisation. This endpoint calls the hot-keys orchestrator partner-assign-key endpoint.
# Partner - Assign a new key with environment specification
Source: https://developer.coincover.com/api-reference/keys/partner--assign-a-new-key-with-environment-specification
/specs/wallet-provider.openapi.json post /v1/partner/keys
Creates a new key pair with the enclave, shards the private key, and securely stores the encrypted key shards. Includes key environment specification: HOT (online generation/storage) or COLD (offline generation/storage). Partner-only endpoint.
# Verify a key's integrity
Source: https://developer.coincover.com/api-reference/keys/verify-a-keys-integrity
/specs/institutional.openapi.json get /v1/keys/{keyId}/verify
Verifies that a key's shards can be reconstructed and that the key pair is valid.
# Get verification status
Source: https://developer.coincover.com/api-reference/verification/get-verification-status
/specs/retail.openapi.json get /v1/verification/status
Retrieves the current status of a biometric verification. The status can be in multiple states: 'pending' (initial state), 'approved' (all verifications passed), 'declined' (verifications failed), or 'review' (marked for manual review).
# Simulate biometric verification events
Source: https://developer.coincover.com/api-reference/verification/simulate-biometric-verification-events
/specs/retail.openapi.json post /v1/verification/simulate
Simulates biometric verification events for testing in non-production environments.
# Start identity verification
Source: https://developer.coincover.com/api-reference/verification/start-identity-verification
/specs/retail.openapi.json post /v1/verification/start
Initiates a new identity verification workflow and returns a Verification ID. The user will use this verification_id to spin up a front-end SDK that will guide the user through the identity verification workflow.
# Changelog
Source: https://developer.coincover.com/changelog/changelog
New features, fixes, and improvements to CoinCover.
**New features**
* **Passwordless sign-in with magic links** — You can now sign in without a password using a secure single-use link sent to your email, with multi-factor authentication still applied.
* **Okta single sign-on** — Organisations can now sign up and sign in to Control using their own Okta identity provider, with the option to require Okta-only sign-in or allow either Okta or a password.
**Improvements**
* Refined the magic-link sign-in experience, including a dedicated sign-in methods view and smoother account resolution.
* Improved Okta single sign-on setup and reliability, including clearer per-organisation sign-in options and more informative provisioning.
* Assigning an approval control list now includes a confirmation step, so changes aren't lost by navigating away before saving.
* Improved approval control list management, including more reliable undo of pending changes.
* Multi-factor authentication entries are now automatically labelled with your role and email, making authenticator apps easier to manage.
* "New Backup Received" notification emails now include the organisation name, making it easier to identify which account a backup belongs to.
* Improved the reliability of identity verification, including cases where a verification record has been redacted.
**Bug fixes**
* Fixed the account reset and re-registration flow, where the "Check your email" step did not include a field to enter the verification code.
**Improvements**
* Refined the wording on the sign-in screen for a clearer experience.
* Improved API key creation, with partner selection scoped to onboarded partners.
* Behind-the-scenes infrastructure improvements to support upcoming capabilities and keep the platform secure and reliable.
**Bug fixes**
* Fixed an issue where a vault sync could display a failure message even though the synchronisation had completed successfully.
**New features**
* **Automatic account cleanup** — When a person is removed from their last organisation, their account is now cleaned up automatically and any outstanding invitations are cancelled. Their email address is released so they can be invited and sign up cleanly again in future, keeping your member list accurate.
**Improvements**
* Refreshed the sign-in experience with a cleaner, more consistent layout.
* You can now download the QR code when setting up multi-factor authentication.
* Behind-the-scenes reliability and infrastructure improvements to keep the platform stable as it scales.
**Improvements**
* Improved the reliability and precision of vault synchronisation.
**New features**
* **Tenant groups** — Users can now be organized into tenant groups, with each user belonging to a single group. This gives you clearer access boundaries across your account.
* **More reliable identity verification** — Resuming an interrupted identity check now works smoothly, and sessions that have gone stale or expired are handled gracefully so you can always start fresh without getting stuck.
**Improvements**
* Access control and recovery approvals now include an additional authentication step for sensitive actions.
* Simplified device registration — adding a second device now only requires hardware authentication.
* Improved the accuracy of key environment (hot/cold) status reporting.
* Improved handling of duplicate partner and organization names to prevent errors during setup and updates.
* Refreshed email templates for user invitations and identity verification reminders, with more reliable handling of embedded images and assets.
* Improved deal creation and updates with more dependable backup handling.
**Bug fixes**
* Fixed an issue where cancelling and reopening the identity verification window could interrupt completing verification.
**Security & reliability**
* Routine security and dependency updates.
**Bug fixes**
* Fixed an issue where the identity verification window would not open after selecting Get started, so verification can now be completed without interruption.
**New features**
* **Organisation security policy management** — Organisation administrators can now configure and enforce security policies for their members, giving teams greater control over authentication requirements and access standards across their organisation.
**Bug fixes**
* Fixed an issue where pending access control change requests were not appearing on the Access Control list page.
**New features**
* **Hardware Authentication for Organisations** — Organisations can now enforce hardware security key requirements for their members. Administrators can configure the minimum and maximum number of required hardware devices per policy, and members are guided through a step-by-step onboarding flow to register their keys. Step-up verification using a registered hardware key is now required when approving sensitive access control changes.
* **Enhanced Identity Verification Traceability** — Identity verification records now include additional user and organisation context, making it easier to investigate and audit verification outcomes within your organisation.
**Improvements**
* Automated daily reminder emails are now sent to users who have not yet completed identity verification, reducing the need for manual follow-up.
* Pending organisation invitations are now automatically re-sent on a regular schedule until the recipient completes signup.
* Administrators can now reset a user's account credentials directly from the admin panel.
* Administrators can now update their email address through account settings.
* Vault Sync CSV exports have been updated to match the format required by the Fireblocks Recovery Utility, preventing import errors during recovery.
* Identity verification webhook handling has been made more resilient — unexpected or unrecognised events are now gracefully acknowledged rather than producing errors.
* Partner reminder email configuration has been simplified, improving reliability across different partner setups.
**Security & reliability**
* Routine dependency and security updates.
**New features**
* **User management for administrators** — Administrators can now manage the user directory and control who belongs to each organisation directly from the platform. Every change is captured in an audit trail.
* **Step-up authentication and hardware-backed authorization** — Sensitive actions can now require step-up verification, with support for hardware-based authentication for an additional layer of protection.
* **Expanded asset support** — A new asset registry broadens the range of supported assets across the platform.
**Bug fixes**
* Resolved inconsistencies in naming across access controls for a clearer, more predictable experience.
**Improvements**
* Backup details can now be edited after creation, including additional fields.
* Recovery notifications are now also sent to Coincover administrators when a recovery is triggered, improving visibility during critical events.
* Improved rate limiting accuracy for traffic served through our content delivery network.
**Security & reliability**
* Routine dependency and security updates.
**Improvements**
* Restored the ability to edit an organisation's environment settings. This option had become unavailable and is now working again.
**New features**
* **Private Key Download as PEM Format** — Recovery packages now include your RSA private key as a PEM file, making it directly compatible with standard cryptographic tools, hardware wallets, and key management systems.
* **Secure Storage Public Key Download** — You can now download the public key associated with your secure storage directly from the portal.
**Bug fixes**
* Fixed an issue where accessing the member list for a backup would incorrectly redirect you to the sign-in page.
* Fixed an issue where backup details only displayed one storage location when multiple storage files were associated with the backup.
* Fixed an issue where historical recovery request details could not be retrieved in certain scenarios.
* Resolved an issue where identity verification could become unresponsive under certain network conditions.
**Improvements**
* Improved the reliability of the cold key recovery process, including more robust upload handling for recovery data packages.
* Backup detail views now reflect each user's access permissions.
* Improved application performance through database query optimisations, resulting in faster load times for user-related operations.
* Improved handling of large file uploads within the recovery workflow.
* Improved partner data access controls for clearer organisation-level scoping.
**Security & reliability**
* Routine dependency and security updates.
# Set up your Access Control List
Source: https://developer.coincover.com/guides/control/approval-control-list
Decide who can approve the recovery of a backup, and how that Access Control List changes — by a majority vote of its own members.
An Access Control List (ACL) is the group of named people authorised to approve the recovery of a backup. Each backup has its own ACL.
Changing who's on an ACL isn't an administrative action. The people already on the list decide, by a simple majority. That's what ensures no single person — not one of your own administrators, and not CoinCover — can quietly change who is able to release your material.
## At a glance
| | |
| ---------------------------- | --------------------------------------------------- |
| **Scope** | One ACL per backup |
| **Minimum size** | At least three members |
| **Who approves a change** | A simple majority of the current members |
| **Identity check** | Every member verifies their identity when they vote |
| **Who can propose a change** | An Organisation Owner, or a member of the ACL |
## How approval works
A change to an ACL — adding, removing or replacing a member — is approved by a simple majority of the people currently on it. Each member verifies their identity at the moment they vote, so an approval is tied to a verified person at the time it's given, not just to someone who signed in earlier. Where your organisation has hardware security keys enabled, members also tap a registered security key as they vote — an extra physical confirmation on top of the identity check.
## Set up your first ACL
Everyone you want on the ACL first needs a Control account. Invite them from the **Team** page and have them complete account setup and identity verification. Only active users of your organisation can be added to an ACL.
While an ACL is being established, you add its first members and approve them in one step. Add everyone who should be on it, then approve.
An ACL needs at least three members before it can be used to approve a recovery. Once it's established, every later change goes to a vote.
Choose your ACL size with a margin. To remove or replace someone on a three- or four-member list, every remaining member has to approve — so a list of five or more keeps approvals moving even when someone is away.
## Change who's on an ACL
Once an ACL is established, adding, removing or replacing a member is proposed and then voted on.
On the backup, open its Access Control List and propose adding, removing or replacing a member.
Everyone currently on the ACL is asked to approve or reject, and each verifies their identity as they vote. The person being removed or replaced doesn't vote on their own change.
Once a simple majority approves, the change is applied. See [Approve or reject an ACL change](/guides/control/approve-list-change) for the voter's side.
## Good to know
* **One change at a time.** Finish or cancel a pending change before proposing another.
* **ACLs are frozen during a recovery.** A backup's ACL can't be edited while a recovery is running on it, and a recovery can't be started while a change to its ACL is in progress. Plan membership changes outside recovery windows.
* **Remove people from the ACL before the organisation.** Control won't remove a person from your organisation while they're still on any Access Control List, so take them off their ACLs first.
## What's next
The voter's side of a membership change.
Use the ACL to approve a recovery.
Onboard the people who'll be on your ACLs.
# Approve or reject an Access Control List change
Source: https://developer.coincover.com/guides/control/approve-list-change
As a member of an Access Control List, review and vote on a proposed change to who can approve a backup's recovery.
When someone proposes adding, removing or replacing a member of a backup's Access Control List (ACL), everyone already on that ACL is asked to vote. A simple majority decides. This guide is for that vote.
## You'll be notified
When a change needs your vote, you're alerted two ways:
* **By email**, with the subject **Action Required: Approve ACL Change**. The email names the backup and links straight to Control.
* **In Control**, on the notifications bell at the top right.
Both take you to the pending change.
## Vote on the change
From the email or the bell, open the change to review it.
You'll see the member being replaced or removed and the proposed new member. Check it's correct before you vote.
You complete an identity check as part of voting. It confirms it's really you approving the change, at the moment you approve it. If your organisation has hardware security keys enabled, you'll also tap your registered security key as you vote.
Your vote counts toward the majority. Once a simple majority approves, the change takes effect; if a majority reject, it's declined.
Your vote is final once you confirm it — it can't be changed afterwards. Review the change carefully before confirming.
The person being added, removed or replaced doesn't vote on their own change. Everyone else currently on the ACL does.
## What's next
How ACLs are created and how changes are proposed.
The other thing an approver is asked to vote on.
Set up your account and find your way around Control.
# Approve or reject a recovery request
Source: https://developer.coincover.com/guides/control/approve-recovery-request
As a member of an Access Control List, review a recovery request, re-verify your identity, and cast your vote.
When a recovery is requested for a backup, everyone on that backup's Access Control List (ACL) is asked to approve it. A simple majority is required before any material is released. This guide is for that approval.
## You'll be notified
A pending recovery reaches you two ways — an email, and the notifications bell at the top right of Control. Both take you to the request.
## Approve or reject
From the email or the bell, open the recovery request.
You'll see the backup, the type of recovery, and how many approvers have voted so far.
Before your approval is recorded, you complete a fresh identity check. This confirms it's really you approving, at the moment of approval — a recent sign-in isn't enough on its own. If your organisation has hardware security keys enabled, you'll also be asked to tap your registered security key as part of this step.
Approve to add your vote toward the majority, or reject to decline the recovery.
Your vote is final once you confirm it. Review the request carefully before confirming.
## What happens next
Once a simple majority of the ACL has approved, the recovery is authorised and the encrypted package is prepared for the requester to download. Only members of that backup's ACL can approve its recovery.
## What's next
The requester's side of a recovery.
The other thing an approver is asked to vote on.
Set up your account and find your way around Control.
# Create a GPG key pair
Source: https://developer.coincover.com/guides/control/create-gpg-key
Generate the GPG key pair you use to encrypt a recovery, and decrypt the package CoinCover returns.
When you recover key material through CoinCover Control, you supply a **GPG public key** with the request. CoinCover encrypts the recovered material to that key, so only the holder of the matching **private key** can open it. This guide shows you how to create the pair, hand the public key to CoinCover, and decrypt the package once it's returned.
CoinCover never receives or holds your private key. If you lose the private key or its passphrase, the recovered material can't be opened — by you or by CoinCover — and you'd have to raise a new recovery. Decide who will hold the private key, and back it up, before you need it.
## Choose a tool
GPG is an open standard with free tools for every platform:
* **macOS** — [GPG Suite](https://gpgtools.org/), which installs the **GPG Keychain** app.
* **Windows** — [Gpg4win](https://www.gpg4win.org/), which installs the **Kleopatra** app.
* **Any platform** — the `gpg` command line.
## Generate your key pair
Download GPG Suite from [gpgtools.org](https://gpgtools.org/), install it, and open the **GPG Keychain** app.
Click **New** on the toolbar. Enter your name and the email address you want the key associated with, set a strong passphrase, and click **Create Key**.
Right-click your new key in the list and choose **Export**. Make sure **Include secret key in exported file** is **unticked** — you're sending only the public half. Save the `.asc` file.
Download Gpg4win from [gpg4win.org](https://www.gpg4win.org/), install it, and open **Kleopatra**.
Choose **File → New Key Pair → Create a personal OpenPGP key pair**. Enter your name and email, set a strong passphrase, and click **Create**.
Right-click your certificate and choose **Export** (not **Export Secret Keys**). Save the `.asc` file — this is your public key.
Generate the pair, following the prompts — choose RSA, 4096 bits, and set a passphrase:
```bash theme={null}
gpg --full-generate-key
```
Export the public key to a file to send to CoinCover:
```bash theme={null}
gpg --armor --export you@example.com > coincover-public.asc
```
Your private key stays in your local keyring. Back it up somewhere secure.
## Give the public key to CoinCover
When you [start a recovery request](/guides/control/recover-key-material), paste the public key or upload the exported `.asc` file. Control checks the key is valid before the request goes ahead.
Only ever share the **public** key. Your private key never leaves your machine and is never sent to CoinCover.
## Decrypt the recovery package
Once your recovery is approved and you've downloaded the package from Control, decrypt it with the private key you already hold.
In Finder, right-click the package CoinCover returned and choose **Services → OpenPGP: Decrypt File** (installed by GPG Suite). Enter your key passphrase when prompted. A decrypted copy is written alongside the original.
In File Explorer, right-click the package and choose **Decrypt and verify**, or open **Kleopatra**, choose **Decrypt/Verify**, and select the file. Enter your passphrase. Kleopatra writes the decrypted output next to the file.
```bash theme={null}
gpg --output recovery-decrypted --decrypt recovery-package.gpg
```
Enter your passphrase when prompted. The decrypted material is written to the output file.
The package can only be opened on a machine that holds the private key matching the public key you supplied, by someone who knows its passphrase. In Control, any member with recovery permission can download the package — but only the holder of the private key can decrypt it. If a different person will collect and open it, make sure they hold that private key.
## What's next
Where you supply this public key and collect the encrypted package.
The approver's side of a recovery.
# Get started with CoinCover Control
Source: https://developer.coincover.com/guides/control/get-started
From your invitation to your first sign-in — creating your account, setting up two-factor authentication, and completing identity verification.
CoinCover Control is where your organisation manages its backups, decides who can approve a recovery, and runs recoveries when a key is lost or compromised. It's non-custodial by design: no single person — not an administrator, and not CoinCover — can release your material on their own.
This guide takes a new user from an invitation through to their first sign-in. There are two roles. **Organisation Owners** manage the team and set up who can approve recoveries. **Members** approve recoveries, and vote on changes to the backups they're responsible for.
## Before you start
You need an invitation — you can't self-register. An Organisation Owner (or CoinCover) invites you by email.
## Set up your account
You'll receive an invitation email from CoinCover. Open it and select **Get started** to open the sign-up page. Use the same email address the invitation was sent to.
Enter your name and choose a password. Passwords need at least 8 characters, and must include an uppercase letter, a lowercase letter, a number, and a special character.
CoinCover sends a verification code to your email address. Enter it to confirm your account. If it doesn't arrive, request a new code and check your spam folder.
Every account uses an authenticator app as a second factor. Scan the on-screen QR code with an app such as Google Authenticator, Authy, 1Password or Microsoft Authenticator — or enter the key manually — then enter the code it generates.
Keep your authenticator safe. If you lose access to it, contact CoinCover support to regain access to your account.
On your first sign-in you complete an identity check. It runs in your browser and takes a few minutes. Identity verification protects every sensitive action in Control, so it has to be completed before you can use the platform.
Depending on how your organisation is set up, you may sign in with your own single sign-on instead of a password. If so, follow your provider's prompts — the rest of Control works the same way.
## Finding your way around
Once you're in, you land on the **Backups** page, which lists every backup your organisation holds. The sidebar has four areas:
| Area | What it's for |
| ------------------------ | ------------------------------------------------------------------------------- |
| **Backups** | Every backup package your organisation holds. This is the default landing page. |
| **Access Control Lists** | Who can approve the recovery of each backup, and any changes to those lists. |
| **Recoveries** | Every recovery, in progress and past. |
| **Team** | The people in your organisation. Owners invite and remove users here. |
The bell at the top right shows anything waiting on you — a recovery to approve, or a change to an Access Control List you're on.
## What's next
Decide who can approve the recovery of a backup.
Get your backed-up material released when a key is lost.
Review and vote on a recovery as an approver.
How institutional recovery is integrated and gated.
# Recover key material through CoinCover Control
Source: https://developer.coincover.com/guides/control/recover-key-material
Get your backed-up key material released — raise the request, provide a GPG key, and collect the material once your approvers have approved it.
Recovery is how your organisation gets its backed-up key material released when a key is lost or compromised. It runs through named approvers in CoinCover Control — never automatically, and never by one person alone. Anyone in your organisation can start a recovery, but nothing is released until the backup's Access Control List (ACL) has approved it.
Recovery is a high-stakes operation, and every recovery is recorded. Start one only when there's a genuine need.
## Before you start
* **Your ACL is ready.** The backup needs an established Access Control List of at least three members. See [Set up your Access Control List](/guides/control/approval-control-list).
* **A GPG key pair you control.** You provide the public key when you raise the request, and you need the matching private key to open the recovered material — so have the pair ready before you begin. If you don't have one yet, see [Create a GPG key pair](/guides/control/create-gpg-key).
* **Know whether the key is held online or offline.** Online (hot) and offline (cold) keys release differently, so it's worth confirming which of your keys is which before you need to recover.
## Run a recovery
On the backup you need to recover, start a recovery request and select the package or packages to recover.
Paste or upload your GPG public key. The recovered material is encrypted to it, so only the holder of the matching private key can open it. Control confirms the key is valid before the request goes ahead. New to GPG? See [Create a GPG key pair](/guides/control/create-gpg-key).
Everyone on the backup's ACL is asked to approve. Each verifies their identity as they vote — and, where your organisation has hardware security keys enabled, taps a registered security key too — and a simple majority is required. You can follow who has voted on the recovery's progress page.
For an **online (hot)** key, the encrypted package is prepared automatically once the recovery is approved. For an **offline (cold)** key, CoinCover's team retrieves the offline material first — Control tells you this is happening and notifies you when the download is ready.
Download the encrypted package from Control and decrypt it with your GPG private key. See [Create a GPG key pair](/guides/control/create-gpg-key#decrypt-the-recovery-package) for the decryption steps on macOS, Windows and the command line.
The download is available for a limited time after it becomes ready, and each download link is short-lived — so collect the material promptly once you're notified.
## Who can do what
Anyone in your organisation can start a recovery. Only members of the backup's ACL can approve it — see [Approve or reject a recovery request](/guides/control/approve-recovery-request) for the approver's side.
## What's next
The approver's side of a recovery.
The approvers a recovery depends on.
Have your derivation paths ready in the recovery package.
How institutional recovery is integrated and gated.
# Vault sync
Source: https://developer.coincover.com/guides/vault-sync
Link a Fireblocks workspace to CoinCover Control so the derivation paths for every vault and address are ready before a recovery is ever needed.
Vault sync links a Fireblocks workspace to CoinCover Control. Once it's linked, Control keeps its own record of every vault account and every wallet address in that workspace, and that record is what produces the **wallet derivation paths** returned inside a recovery package.
The two backup flows on the [Fireblocks partner guide](/partner-guides/fireblocks) protect the **keys**. Vault sync protects the **derivation paths** — the record of how each vault and address was derived, which is what's needed to rebuild the accounts once the keys are back.
It applies wherever CoinCover backs up a Fireblocks workspace: [Recover for Institutions](/institutional-recovery/overview) customers backing up their own workspace, and [wallet providers](/wallet-provider-recovery/overview) backing up their customers' on Fireblocks. It has no application to [retail recovery](/retail-recovery/overview), where there is no workspace to enumerate.
Vault sync is optional, and strongly recommended at onboarding.
## Why link it at onboarding
If the workspace was never linked, a recovery still returns your keys. It does not return the derivation paths — nothing in the package records how your accounts were derived, so you would need to export a vault report from Fireblocks at recovery time, or rebuild every account by hand.
A recovery is not something you get to repeat, and there is no way to add derivation paths to a package that has already been assembled. That is the whole argument for completing the link now rather than later.
## At a glance
| | |
| ------------------------ | ------------------------------------------------------------------------------ |
| **Applies to** | Fireblocks workspaces backed up with CoinCover |
| **Who sets it up** | An organisation owner, in CoinCover Control. CoinCover is not involved |
| **Access needed** | A Fireblocks read-only (Viewer) API user, and its private key |
| **What CoinCover reads** | Vault accounts, the assets in each, and the addresses under them. Nothing else |
| **Refresh** | Once a day at a fixed time, plus on demand whenever you want it |
| **Where you receive it** | Inside a workspace recovery package, as a CSV alongside the key material |
Vault sync is set up and refreshed in CoinCover Control. There are no vault sync endpoints in the API reference — it isn't something you build against.
## Before you start
You'll need three things in place:
* **A Fireblocks partner relationship with CoinCover, and a workspace backup against it.** Vault sync is reached from that backup's page in Control, and the controls only appear where the backup's partner is Fireblocks.
* **An Admin-level user in your Fireblocks workspace.** Creating an API user and uploading a CSR is an Admin-level action at Fireblocks. This is done at Fireblocks, not at CoinCover.
* **An organisation owner account in CoinCover Control.** Owners link and change workspace credentials. Every other user in your organisation holds the Member role — a Member can trigger a refresh and browse the synced vaults and addresses, but cannot link or change credentials. The permission is organisation-wide rather than per backup.
The read-only API user is created in the **Fireblocks console**, not in CoinCover Control. You then paste the credentials Fireblocks issues into Control.
## Set it up
In the Fireblocks console, under developer settings, create an API user with the **Viewer** role — the lowest-privilege role that can list vaults and addresses.
CoinCover reads nothing beyond vault accounts and addresses, but nothing at link time inspects the privilege level of the credentials you supply. Keeping the API user at Viewer is your control, not one CoinCover applies for you.
Fireblocks issues the API key against a certificate signing request (CSR) that **you** generate, so the key pair originates on your own machine rather than at Fireblocks or at CoinCover.
Generate an RSA 4096-bit key pair and the CSR on your own machine:
```bash theme={null}
openssl req -new -newkey rsa:4096 -nodes -keyout api_private.key -out api_user.csr
```
OpenSSL prompts for the certificate fields — Common Name (your name, or the API user's), Organisation, Country as a two-letter code, State, Locality, and Organisational Unit, which is optional. You end up with two files: `api_user.csr`, which goes to Fireblocks, and `api_private.key`, which stays with you.
Upload `api_user.csr` in the **CSR File** field as you create the API user in the Fireblocks console, and Fireblocks issues the **API key** against it.
Fireblocks documents this step at [Generate a CSR for an API user](https://developers.fireblocks.com/docs/generate-a-csr-for-an-api-user).
On the backup's page in Control, an organisation owner opens the credentials dialog and pastes the issued API key and the private key. Both are encrypted on arrival and only the encrypted form is stored.
Supply the private key exactly as it was issued against your CSR — complete, and unencrypted.
Linking sets the workspace to pending. Trigger the first refresh from the same screen. It runs in the background rather than inside the request, and the page follows it and updates itself when it finishes.
Check the **refresh status and the refresh history**, not only that a refresh happened. Until a refresh has succeeded, there is nothing in the record to release at recovery.
Control lists your vault accounts with their asset and address counts, and you can open a single vault to see its addresses. Compare this against Fireblocks.
This is the point to catch a workspace linked with credentials scoped to the wrong workspace — CoinCover cannot tell one of your workspaces from another, so any valid credentials will link and sync.
## Keeping it current
A refresh runs **automatically once a day, at a fixed time**. That time is set by CoinCover and is the same for every customer — there is no per-organisation cadence or window. You can also refresh **on demand** from Control at any time, as often as you like.
Refreshes run in the background. Having triggered one, the page updates itself with the vault and address counts when it completes. A large workspace can take longer than the page waits, in which case Control tells you to check back — reload it once the refresh has had time to finish.
Control shows the **recent refresh history** for your workspace, covering whether each was automatic or one you triggered, whether it succeeded, and how many vaults and addresses it found. Failures show the reason.
CoinCover's record is cumulative: it keeps everything it has ever seen in the workspace, including vaults and addresses you later delete at Fireblocks, so those still appear in the derivation paths returned at recovery. Allow for that if you reorganise.
## What CoinCover reads and stores
**Reads:** your vault accounts, the assets held in each, and the addresses under each asset. Nothing else — no balances, no transactions, no transfer history, no policies, no users. CoinCover cannot initiate any movement of assets, and you can revoke the API user at Fireblocks whenever you choose.
**Stores:** the vault accounts, with the identifier Fireblocks issued and the name you gave them there; the wallet addresses, with the asset, the address itself, its type, any tag and its index; the credentials you supplied, as ciphertext only; and one record per refresh showing whether it succeeded and how much it found.
The credentials cannot be read back. They're encrypted the moment they arrive and only the encrypted form is kept, so no screen, interface or CoinCover person can return them. If you've lost your own copy, issue a new API user at Fireblocks and link again.
## If you change or revoke credentials
Saving new credentials overwrites the stored pair, returns the workspace to pending, and the next refresh re-reads the whole workspace from scratch. Your previously synced vaults and addresses are retained throughout, so nothing is lost from what CoinCover holds.
Revoking the API user at Fireblocks does not remove anything from CoinCover. The record stays exactly as it was at the last successful refresh and continues to be used at recovery; it simply stops picking up anything new.
## What you get at recovery
Where the workspace has been synced, a [workspace recovery](/partner-guides/fireblocks#recovery) can return the derivation paths as a CSV alongside the key material — one row per address, giving the vault account and its name, the asset, the address, its type, any tag, and the hierarchical derivation path. The format is aligned to the Fireblocks Recovery Utility, so it can be imported rather than transcribed.
## What's next
The workspace backup, the owner's device share backup, and both recovery flows.
How institutional recovery is integrated, gated and released.
Backing up your own customers' wallets on a supported platform.
Rehearsing a recovery before you need one.
# Welcome to CoinCover
Source: https://developer.coincover.com/index
Pick the product you're integrating with and follow its guide.
CoinCover Recover gives you a recoverable, audit-ready way to release sensitive key material when it's needed back — without ever holding plaintext keys. Material is encrypted before it reaches us; our role is to release it under verified conditions, with a full audit trail.
There are three products. Pick the one that matches your integration:
For wallet platforms, custodians, and exchanges backing up their **customers'** wallets. Integrate the Partner Key Service under the hood — by API or key ceremony — using your platform's native backup and recovery.
For institutions backing up their **own** wallets. Served through a supported platform, or integrated directly — back up a key, add CoinCover as a multi-sig backup key, or use the CLI.
For wallet providers serving **end users**. Identity-verified release tied to a user identifier, with hosted IDV and webhook-driven status. Available via REST API.
## Where to go next
Each product section starts with an overview of the architecture, then walks you through integration end to end, then covers testing in sandbox.
If you already know the product and just want the endpoint catalogue, the API reference tab lists each product's API under its own heading — Wallet Provider, Institutional, and Retail. Each surface has its own authentication and base URLs (and, for Retail, its own event model), so it's documented in full within its own section.
Want to see what's changed recently? The changelog tab tracks new features, fixes, and improvements.
Not sure which product fits? [Talk to us](https://www.coincover.com/contact) and we'll point you at the right starting line.
# Add a backup key
Source: https://developer.coincover.com/institutional-recovery/add-a-backup-key
This guide covers using a CoinCover backup key — `secp256k1` or `ed25519`, depending on your chain — as the **backup key** in your own multi-sig wallet. CoinCover generates and holds the private key inside its enclave and signs only at recovery — so a lost or compromised key on your side never means lost assets.
If instead you want to encrypt recovery material and store the ciphertext with us, see [Back up key material.](/institutional-recovery/back-up-key-material)
The worked example below is the `secp256k1` / BIP32 case. `ed25519` keys (used by chains such as Solana) are also supported as backup keys, but they follow their chain's own hierarchical derivation scheme rather than BIP32 — the chain-code and xpub assembly steps don't apply. The assign, store, verify, and recovery shape is otherwise the same; talk to your account manager for the `ed25519` derivation specifics.
## The xpub challenge (secp256k1)
Most multi-sig platforms expect a BIP32 extended public key (xpub) for each key in the wallet. An xpub is `(compressed public key + 32-byte chain code)` plus BIP32 metadata. CoinCover's enclave returns the compressed public key but not a chain code, so there's no native xpub — you supply the chain code, assemble the xpub yourself, and store the chain code back with CoinCover so it's available at recovery.
## Step 1 — Authenticate
Authenticate with a Bearer token in the `Authorization` header — a JWT or a long-lived API key.
```bash theme={null}
export COINCOVER_API_KEY=""
export COINCOVER_BASE_URL="https://service.uat-keys.coincover.com"
```
## Step 2 — Assign the signer key
Call `POST /v1/keys` with `key_type: "secp256k1"` (or `"ed25519"` for chains that use it). CoinCover generates the key pair in the enclave and returns the compressed public key (hex) plus a `signature`. The private key never leaves the enclave.
```bash theme={null}
curl -X POST "$COINCOVER_BASE_URL/v1/keys" \
-H "Authorization: Bearer $COINCOVER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"user_identifier": "treasury-wallet-01",
"key_type": "secp256k1",
"key_environment": "COLD"
}'
```
Verify the returned `signature` against your CoinCover verification key before continuing. If it fails, treat the response as untrusted and escalate before building the wallet.
### Optionally, bind business context into the signature
You can also bind business context into that signature so it attests to *which* wallet or customer the key is for, not just the key bytes. Pass a `sign_with` array on the request and the enclave signs the fields you name alongside the `public_key`. For a backup signer the useful fields are `key_fingerprint` / `public_key` and `external_customer_id`: `key_fingerprint` is the SHA-256 of the compressed public key the enclave issued, and `public_key` is always in the payload anyway, so together with `external_customer_id` the signature ties that enclave-issued key to the customer it's for. The chain code you add when assembling the xpub is outside this signature — store and track it separately. Every field you list must resolve to a value, or the request returns a `400`.
The request shape, the `signed_payload`/`signed_fields` response, and a verification snippet are all in [Back up key material](/institutional-recovery/back-up-key-material#optionally-bind-business-context-into-the-signature) — the mechanics are identical here.
## Step 3 — Generate a chain code
Generate a random 32-byte chain code with a CSPRNG.
```typescript Node theme={null}
import crypto from "node:crypto";
const chainCode = crypto.randomBytes(32); // 32 bytes from a CSPRNG
```
## Step 4 — Assemble the xpub
Build a BIP32 xpub from the enclave's compressed public key plus your chain code. This is the backup key xpub you'll register with your wallet platform.
## Step 5 — Store the chain code with CoinCover
The chain code is recovery-critical: without it, the private key alone can't derive child keys. Store it with CoinCover so it's available at recovery.
`POST /v1/keys` with `key_type: "rsa4096"` to get an encryption public key.
RSA-OAEP encrypt a small payload such as `{ chainCode, publicKey, coin, label }` with that public key (SHA-256, as in [Back up key material](/institutional-recovery/back-up-key-material)).
`POST /v1/secure/data` with the ciphertext and plaintext checksum. Keep the returned `backup_id`.
## Step 6 — Register the backup key with your wallet platform
Register the assembled xpub as the **backup key** in your wallet, following your platform's own key-ceremony process. The exact API calls vary by platform, but the shape is consistent:
Create the keys you control locally (e.g. your primary signing key), following your platform's guidance.
Register your own public key(s), the CoinCover backup xpub from step 4, and any platform-held key, per your wallet's multi-sig scheme.
Run your platform's key ceremony to bind the keys into the wallet.
You only ever persist the key material **you** control, in your own secrets store. The backup private key lives solely in CoinCover's enclave — never store it, and don't create empty placeholders for it.
## Step 7 — Record references
In your own database, store the references you'll need at recovery time:
* `coincoverBackup.keyId` — the signer key ID from step 2
* `coincoverBackup.backupId` — the chain-code backup ID from step 5
## Step 8 — Round-trip verify before production
Also confirm the key and chain-code backup with the self-service `verify` endpoints:
```bash theme={null}
curl "$COINCOVER_BASE_URL/v1/keys/$KEY_ID/verify" \
-H "Authorization: Bearer $COINCOVER_API_KEY"
curl "$COINCOVER_BASE_URL/v1/backups/$BACKUP_ID/verify" \
-H "Authorization: Bearer $COINCOVER_API_KEY"
```
## Step 9 — Test before you ship
Walk the backup-key scenarios in the [testing page](/institutional-recovery/testing), including a sandbox recovery that reconstructs the backup key from the enclave key plus the stored chain code and signs at your wallet's derivation path.
## What's next
Encrypt recovery material client-side and store it with CoinCover.
Every endpoint, request, and response.
# Back up key material
Source: https://developer.coincover.com/institutional-recovery/back-up-key-material
Encrypt your own recovery material client-side and store it with CoinCover, using an RSA key.
This guide covers the **encrypt-and-store** approach: you encrypt recovery material — seed phrases, key shares, or backup files — client-side with an `rsa4096` key, and store the ciphertext with CoinCover. We never see plaintext, and the private key needed to recover it never leaves our enclave.
If instead you want CoinCover to act as the backup key in a multi-sig wallet, see [Add CoinCover as a backup key](/institutional-recovery/add-a-backup-key).
## Step 1 — Authenticate
Authenticate with a Bearer token in the `Authorization` header — a JWT or a long-lived API key.
```bash theme={null}
export COINCOVER_API_KEY=""
export COINCOVER_BASE_URL="https://service.uat-keys.coincover.com"
```
## Step 2 — Choose HOT or COLD
You choose the environment per key, and it's fixed for the life of the key.
| Environment | When to use it |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `HOT` | Online generation and storage. Best for high-frequency operations and low-latency access. |
| `COLD` | Offline generation and storage with enhanced isolation. Best for high-value wallets and the strictest security requirements. |
## Step 3 — Assign a key
Call `POST /v1/keys` with `key_type: "rsa4096"`. Only `user_identifier` and `key_type` are required; supply `organisation`/`package` only if you want to file the key against your own internal structure.
```bash curl theme={null}
curl -X POST "$COINCOVER_BASE_URL/v1/keys" \
-H "Authorization: Bearer $COINCOVER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"user_identifier": "treasury-wallet-01",
"key_type": "rsa4096",
"key_environment": "COLD"
}'
```
```typescript Node theme={null}
const res = await fetch(`${baseUrl}/v1/keys`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
user_identifier: "treasury-wallet-01",
key_type: "rsa4096",
key_environment: "COLD",
}),
});
const { key_id, public_key, signature } = await res.json();
```
```python Python theme={null}
import requests
res = requests.post(
f"{base_url}/v1/keys",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
json={
"user_identifier": "treasury-wallet-01",
"key_type": "rsa4096",
"key_environment": "COLD",
},
)
key = res.json()
key_id, public_key = key["key_id"], key["public_key"]
```
Store the `key_id` against your wallet record. The response also carries a `signature` over the `public_key`, signed by the generating enclave — verify it with your CoinCover verification key before relying on the key. If verification fails, treat the response as untrusted and escalate.
### Optionally, bind business context into the signature
By default the signature covers only the `public_key`. Pass a `sign_with` array on the request to have the enclave also bind chosen context fields — alongside the `public_key`, which is always included — so a verified signature attests to *which* wallet, customer, or package the key was issued for, not just the key bytes.
Supported fields: `external_customer_id`, `external_package_id`, `pulled_by_id`, `pulled_by_type`, `user_id`, `key_id`, `key_fingerprint`. To bind `external_customer_id` or `external_package_id`, include the matching `organisation`/`package` on the request. Don't list `public_key` — it's always included implicitly, and passing it is rejected. Any field you list that can't be resolved returns a `400`. `key_fingerprint` is the SHA-256 (hex) of the public-key bytes; `pulled_by_id`/`pulled_by_type` are the caller identity CoinCover sets from your auth token, not request fields.
```bash theme={null}
curl -X POST "$COINCOVER_BASE_URL/v1/keys" \
-H "Authorization: Bearer $COINCOVER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"user_identifier": "treasury-wallet-01",
"key_type": "rsa4096",
"key_environment": "COLD",
"organisation": { "customer_id": "acme-treasury" },
"sign_with": ["external_customer_id", "user_id", "key_fingerprint"]
}'
```
When `sign_with` is present, the response carries two extra fields alongside `signature`:
```json theme={null}
{
"key_id": "b1f7c0de-2a4c-4f1e-9c3a-8d2e6b5a1f04",
"public_key": "3082020a...",
"signature": "MEUCIQD...base64...",
"signed_payload": "{\"external_customer_id\":\"acme-treasury\",\"key_fingerprint\":\"9f86d081...\",\"public_key\":\"3082020a...\",\"user_id\":\"5e9b1c74-3a80-4d2e-b1f6-0c7a9e2d4415\"}",
"signed_fields": ["external_customer_id", "user_id", "key_fingerprint"]
}
```
`signed_payload` is the exact [JCS-canonical](https://www.rfc-editor.org/rfc/rfc8785) JSON string the enclave signed: object keys are sorted and the `public_key` is always included alongside your requested fields. `signed_fields` echoes the fields you requested, in request order.
Verify `signature` against your CoinCover verification key over those exact bytes — don't re-serialise the JSON — then confirm the bound values match the wallet you're provisioning. A failed signature or an unexpected value means the response is untrusted: stop and escalate.
`sign_with` is optional and additive. Omit it and the response is unchanged — the legacy signature covers the `public_key` alone, verified over the UTF-8 bytes of the hex `public_key` string (not its decoded bytes) — so existing integrations keep working.
## Step 4 — Encrypt client-side
The public key comes back hex-encoded. Convert it to PEM, then encrypt with RSA-OAEP using SHA-256 as both the hash and MGF1 hash.
The checksum must be SHA-256 of the **plaintext**, not the ciphertext. It's optional on the standard endpoint, but supplying it lets CoinCover verify the recovered plaintext on retrieval — we recommend always sending it.
```typescript Node theme={null}
import crypto from "node:crypto";
function hexToPem(publicKeyHex: string): string {
const der = Buffer.from(publicKeyHex, "hex");
const b64 = der.toString("base64").match(/.{1,64}/g)!.join("\n");
return `-----BEGIN PUBLIC KEY-----\n${b64}\n-----END PUBLIC KEY-----\n`;
}
function encrypt(publicKeyHex: string, plaintext: Buffer) {
const pem = hexToPem(publicKeyHex);
const ciphertext = crypto.publicEncrypt(
{
key: pem,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: "sha256",
},
plaintext,
);
const checksum = crypto.createHash("sha256").update(plaintext).digest("hex");
return { data: ciphertext.toString("base64"), checksum };
}
```
```python Python theme={null}
import base64
import hashlib
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
def hex_to_pem(public_key_hex: str) -> bytes:
der = bytes.fromhex(public_key_hex)
b64 = base64.encodebytes(der).decode().strip()
return f"-----BEGIN PUBLIC KEY-----\n{b64}\n-----END PUBLIC KEY-----\n".encode()
def encrypt(public_key_hex: str, plaintext: bytes) -> dict:
pem = hex_to_pem(public_key_hex)
public_key = serialization.load_pem_public_key(pem)
ciphertext = public_key.encrypt(
plaintext,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
),
)
return {
"data": base64.b64encode(ciphertext).decode(),
"checksum": hashlib.sha256(plaintext).hexdigest(),
}
```
RSA-OAEP has a maximum message size (around 446 bytes for a 4096-bit key with SHA-256). For anything larger, encrypt the payload with a fresh symmetric key and wrap that symmetric key with the public key — or use the file endpoint.
## Step 5 — Store encrypted data
Send the ciphertext, the plaintext checksum, and the public key to `POST /v1/secure/data`.
```bash theme={null}
curl -X POST "$COINCOVER_BASE_URL/v1/secure/data" \
-H "Authorization: Bearer $COINCOVER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"public_key": "04a1b2c3d4e5f6789...",
"checksum": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"data": "eyJrZXkiOiJ2YWx1ZSJ9",
"padding_type": "OAEP"
}'
```
You get back a `backup_id`. Store it — it's the handle to find this exact backup again.
For binary backups — encrypted archives, wallet seed files — use `POST /v1/secure/file` instead. It's `multipart/form-data`, expects the file to already be encrypted on your side, and returns a `backup_id` the same way.
## Step 6 — Verify the key and backup
Confirm integrity with the self-service `verify` endpoints, right after assignment and storage and periodically thereafter as a drill.
```bash theme={null}
# Confirm the key's shards reconstruct and the pair is valid
curl "$COINCOVER_BASE_URL/v1/hot/keys/$KEY_ID/verify" \
-H "Authorization: Bearer $COINCOVER_API_KEY"
# Confirm a stored backup is valid and recoverable
curl "$COINCOVER_BASE_URL/v1/hot/backups/$BACKUP_ID/verify" \
-H "Authorization: Bearer $COINCOVER_API_KEY"
```
`verify-key` returns `is_key_valid`; `verify-backup` returns `is_backup_valid`. Treat a `false` from either as a hard failure and escalate before relying on the material.
## Step 7 — Test before you ship
Run through the going to production checklist before cutting production traffic to live keys. The most common issues:
* Hex-to-PEM conversion off by one byte (verify with a known-good public key first)
* OAEP hash and MGF1 hash mismatched (both should be SHA-256)
* Checksum computed over ciphertext rather than plaintext
* Sandbox base URL still set in production config
* `key_environment` defaulted to `HOT` where you intended `COLD`
The [testing page](/institutional-recovery/testing) has the full sandbox matrix.
## What's next
Use a secp256k1 key as the backup key in your multi-sig or MPC wallet.
Every endpoint, request, and response.
# CLI integration guide
Source: https://developer.coincover.com/institutional-recovery/cli-integration-guide
The CoinCover Key Vault CLI replaces the legacy `coincover/crypt` Docker image. It lets you:
* **Back up** private key files by encrypting them on your machine and uploading only ciphertext to CoinCover.
* **Recover** key material locally after CoinCover Control has released a recovery package and your approval policy is satisfied.
Your plaintext key material never leaves your machine during backup. Recovery is performed entirely on your machine once you have the released files — no network is required for the recover flow.
The CLI is **interactive**: there are no subcommands today. Run the binary, choose an action from the menu, and follow the prompts. Many steps include a **? Help** option with more context.
For the full menu, environments, size limits, config, and troubleshooting tables, see the [CLI reference](/institutional-recovery/cli-reference).
## What you need before you start
### For every session
| Requirement | Notes |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| **API key** | Obtain from CoinCover as part of your onboarding or Control setup. The CLI stores one key per environment on your machine. |
| **Supported platform** | macOS (Intel or Apple Silicon), Linux (x86\_64 or ARM64), or Windows x64. |
### For backup
| Requirement | Notes |
| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Network** | The CLI calls CoinCover's keys API for the environment you select. |
| **Backup name** and **Backup ID** | You enter these during the flow; they identify the package in CoinCover. |
| **Control ID** | Your CoinCover Control workspace identifier for this backup. |
| **Key file** | **RSA + passphrase:** up to **401 bytes**. **RSA only:** up to **445 bytes**. **Envelope encryption + passphrase:** above **470 bytes**, up to **10 MB** (ZIP upload). |
| **Passphrase custody** (RSA + passphrase and envelope) | By default the CLI generates a strong passphrase; you can create your own that meets the CLI rules. You must save it outside the CLI. CoinCover cannot recover it. |
### For recovery
| Requirement | Notes |
| ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Recovery package** | From CoinCover Control after your approval policy on the original backup is satisfied. |
| **RSA private key** | Unencrypted PKCS#8 PEM (`-----BEGIN PRIVATE KEY-----`). If the package is GPG-wrapped, **decrypt with GPG outside the CLI first**, then point the CLI at the decrypted PEM. |
| **Encrypted backup file** | Small backups: RSA ciphertext (binary or base64). Envelope backups: **ZIP package** from the recovery package. |
| **Backup passphrase** (RSA + passphrase and envelope backups) | The passphrase saved at backup time — not your CoinCover Control login. |
On **macOS**, install with Homebrew (recommended). On **Linux** and **Windows**, download release binaries.
## Install and run
The public tap is [**coincover/key-vault-tap**](https://github.com/coincover/key-vault-tap). Binaries are Developer ID signed and notarized.
```bash theme={null}
brew tap coincover/key-vault-tap https://github.com/coincover/key-vault-tap.git
brew install coincover
coincover
```
Upgrade when a new version is published:
```bash theme={null}
brew update
brew upgrade coincover
```
Release assets and checksums: [github.com/coincover/key-vault-tap/releases](https://github.com/coincover/key-vault-tap/releases).
1. Download `cc-cli-macos-arm64-v0.x.x` (bare binary) or `cc-cli-macos-arm64-v0.x.x.tar.gz` for your chip from [key-vault-tap releases](https://github.com/coincover/key-vault-tap/releases).
2. Run the bare binary:
```bash theme={null}
chmod +x cc-cli-macos-arm64-v0.x.x
./cc-cli-macos-arm64-v0.x.x
```
Or from a tarball:
```bash theme={null}
tar xzf cc-cli-macos-arm64-v0.x.x.tar.gz
./coincover
```
3. **Optional:** install on your `PATH`:
```bash theme={null}
sudo mv coincover /usr/local/bin/coincover
```
**Gatekeeper:** macOS binaries from official releases are Developer ID signed and notarized. If macOS still blocks the app, open **System Settings → Privacy & Security** and allow the binary, or right-click the file → **Open** once to confirm.
1. Download `cc-cli-linux-x64-v0.x.x` or `cc-cli-linux-arm64-v0.x.x`.
2. Make it executable and run:
```bash theme={null}
chmod +x cc-cli-linux-x64-v0.x.x
./cc-cli-linux-x64-v0.x.x
```
3. **Optional:** install to a directory on your `PATH`:
```bash theme={null}
sudo mv cc-cli-linux-x64-v0.x.x /usr/local/bin/coincover
coincover
```
1. Download `cc-cli-win-x64-v0.x.x.exe`.
2. Run from PowerShell or Command Prompt:
```powershell theme={null}
.\cc-cli-win-x64-v0.4.1.exe
```
3. **Optional:** add the folder containing the `.exe` to your user or system `PATH`, or copy the file to a folder already on `PATH`.
Tarballs contain an executable named `coincover` at the archive root. Bare binaries (no extension) use the `cc-cli-*` filename — rename to `coincover` when adding to your `PATH`. Each release may include `checksums.txt` (and Cosign signature files on internal releases) for verification.
### Optional: verify checksums
From the directory where you downloaded the release assets:
```bash theme={null}
cd /path/to/downloads
sha256sum -c checksums.txt
```
On macOS without GNU coreutils, use `shasum -a 256` and compare manually against `checksums.txt`.
### Running the CLI
* No command-line flags are required. Starting the binary shows **CoinCover Key Vault** and the main menu.
* Use **arrow keys** and **Enter** to select options.
* Press **Esc** or **Ctrl+C** to cancel the current prompt (individual flows exit cleanly where noted below).
* Choose **? Help** on many screens for in-app explanations.
## First-time setup
The first time you run the CLI (or whenever required settings are missing), you are guided through setup before the main menu appears.
Choose which CoinCover API environment to use — `dev`, `staging`, `uat`, or `prod`. Each maps to a specific API host. Environments that already have a saved API key are marked **(key saved)** in the list.
Enter your partner API key for the selected environment which was provided by CoinCover as part of onboarding. Input is masked. The key is saved locally for that environment so you are not prompted again unless you switch environments or update the key from the menu.
Enter your partner API key for the selected environment which was provided by CoinCover as part of onboarding. Input is masked. The key is saved locally for that environment so you are not prompted again unless you switch environments or update the key from the menu.
Enter defaults used on future backups:
* **User ID** — maps to `user_identifier` in the partner API.
* **Customer name** — maps to `organisation.customer_name`.
You can override these for a single backup later, or change defaults from the main menu.
After setup, the CLI prints the active **environment** (with API URL) and **saved identity** before each menu cycle. Settings are stored at `~/.coincover/config.json`.
## Back up key material
Choose **Back up key material** from the menu. The flow asks a series of questions, then encrypts and uploads. You can cancel with **Esc** / **Ctrl+C** until you confirm on the review screen — declining review or cancelling earlier performs **no API calls**.
The CLI shows your saved **User ID** and **Customer name** and asks **Use this User ID and Customer name for this backup?**
* **Yes** — use saved values for this run only.
* **No** — enter different values (fields are pre-filled). You may then choose **Save these as your default identity for future backups?**
* **HOT key pair** — standard online backup; key is generated in CoinCover's online enclave.
* **COLD key pair** — air-gapped or offline-oriented workflows.
This choice is **permanent for this backup** — it cannot be changed afterward for the same package.
* **RSA + passphrase (recommended)** — small files (up to **401 bytes**). Two layers; uploads via secure data; enclave checksum verified.
* **RSA only** — small files (up to **445 bytes**). One layer; no passphrase.
* **Envelope encryption + passphrase** — large files (**above 470 bytes**, up to **10 MB**). Passphrase plus DEK envelope; uploads a **ZIP** via secure file.
Default is RSA + passphrase. Use **? Help** on this screen for size limits and upload paths.
* **Backup name** — human-readable label (e.g. `q2-treasury-key`).
* **Backup ID** — package identifier (e.g. `p1`).
An interactive file browser opens. The list supports **type-to-filter** and **↑↓** to choose; **`...`** means more names are off-screen. Select **`../`** **(Back)** to go up a directory, entries ending in **`/`** to enter folders, and a file to use it.
The file must be non-empty and within the size limit for your protection mode. Envelope mode requires files **larger than 470 bytes**.
A summary shows protection, names, file path, RSA key type, and identity. You are then prompted for **Control ID** (your workspace identifier in CoinCover Control). **You will receive a Control ID for each backup that you purchase from CoinCover.** The summary is shown again with Control ID included.
A summary shows protection, names, file path, RSA key type, and identity. You are then prompted for **Control ID** (your workspace identifier in CoinCover Control). **You will receive a Control ID for each backup that you purchase from CoinCover.** The summary is shown again with Control ID included.
1. Choose **CLI-generated passphrase (recommended)** or **Create my own passphrase** (default: generated).
2. **Generated path:** the CLI shows a random passphrase in a **Save this passphrase** note with important warnings, then asks you to **type or paste the same passphrase** again to confirm you saved it.
3. **Custom path:** a **Backup passphrase rules** note lists requirements (16–128 characters, at least three character classes, at least 10 distinct characters, no common passwords or obvious patterns). You enter the passphrase twice via masked prompts; the CLI validates each entry.
**RSA only** skips this step.
A review panel lists all choices. **Proceed?** defaults to **No**. If you confirm **Yes**, the CLI:
1. Allocates a CoinCover HOT or COLD RSA-4096 key via the API.
2. Encrypts locally (small-file: full RSA wrap + secure data; envelope: ZIP package + secure file).
3. For small-file backups, verifies the server checksum; envelope file uploads do not use the same checksum step.
On success you see **backup\_id**, **key\_id**, key type, environment, backup name, and the path to a saved **receipt** under `~/.coincover/receipts/`. **Keep your passphrase safe** if you used a passphrase mode.
A review panel lists all choices. **Proceed?** defaults to **No**. If you confirm **Yes**, the CLI:
1. Allocates a CoinCover HOT or COLD RSA-4096 key via the API.
2. Encrypts locally (small-file: full RSA wrap + secure data; envelope: ZIP package + secure file).
3. For small-file backups, verifies the server checksum; envelope file uploads do not use the same checksum step.
On success you see **backup\_id**, **key\_id**, key type, environment, backup name, and the path to a saved **receipt** under `~/.coincover/receipts/`. **Keep your passphrase safe** if you used a passphrase mode.
## Recover key material
Choose **Recover key material** from the menu. This flow is **local only** — it does not call the CoinCover API after you have the files. You need a recovery package from CoinCover Control **after** the approval policy on the original backup is satisfied.
If files are still GPG-encrypted inside the recovery package, decrypt them with GPG **outside** the CLI first. The CLI expects a **decrypted PEM** RSA private key (not the encrypted backup blob), and the **encrypted backup artifact** from backup time — for envelope backups this is the **`…-backup-package.zip`** file (binary or base64), not the individual files inside an extracted folder.
Browse to the PKCS#8 PEM private key (`-----BEGIN PRIVATE KEY-----` … `-----END PRIVATE KEY-----`).
Browse to the ciphertext file. For **envelope** backups, select the **ZIP** in the recovery folder (e.g. `p1-backup-package.zip`), not `payload.enc` or `wrapped_dek.bin` from inside an extracted copy.
The CLI detects **base64** (whitespace trimmed, then decoded) or **binary** automatically. It does **not** ask which backup type you used — it detects **ZIP** (envelope) or, for small files, inspects the inner buffer after RSA unwrap.
A summary shows private key path, ciphertext path, format, and either **Package format: Envelope … (ZIP)** with **Recovered as** `recovered_`, or **Inner format** for small-file backups.
A note explains this is the **backup passphrase** from when you created the backup — not your CoinCover Control password.
* Enter the passphrase.
* Wrong passphrase: up to **3 attempts** with warnings; then you return to the main menu.
* **RSA only** skips this step.
Recovered plaintext is written next to the encrypted file:
* Small-file (`secure/data`): `recovered_` — e.g. `recovered_validator_key.txt`.
* Envelope (`secure/file` ZIP): `recovered_` from the ZIP manifest — e.g. `recovered_photo.jpeg`.
The recovered file is **not** a ZIP — do not unzip it. Older envelope packages without `original_filename` in the manifest may still use `recovered_`; rename the extension manually or re-backup with a current CLI. If the output file already exists, the CLI asks whether to overwrite (default **No**).
## Managing settings
**Switch environment / API key** — use this before backup work when you need a different CoinCover environment or a new API key. The CLI shows the active environment and API URL after switching.
**Set / update saved User ID & Customer name** — changes global defaults in `~/.coincover/config.json`. They apply across all environments until you change them again. Each backup can still override identity when prompted.
## What's next
Environments, menu, size limits, config, receipts, logging, and troubleshooting.
Use a secp256k1 or ed25519 key as the backup key in your multi-sig or MPC wallet.
# CLI reference
Source: https://developer.coincover.com/institutional-recovery/cli-reference
Lookup tables for the CoinCover CLI. For the install-and-use walkthrough, see the [CoinCover CLI guide](/institutional-recovery/cli-integration-guide).
## Environments and API hosts
During first-time setup (or via **Switch environment / API key**) you choose one of four environments. The CLI stores one API key per environment.
| Environment | API host |
| ----------- | ---------------------------------------- |
| `uat` | `https://service.uat-keys.coincover.com` |
| `prod` | `https://service.keys.coincover.com` |
Environments that already have a saved API key are marked **(key saved)** in the list.
## Main menu
| Menu item | What it does |
| ---------------------------------------------- | ------------------------------------------------------------------- |
| **Back up key material** | Encrypt a key file locally and upload ciphertext to CoinCover. |
| **Recover key material** | Decrypt a released recovery package on this machine (no API calls). |
| **Switch environment / API key** | Point the CLI at another environment or enter a new API key. |
| **Set / update saved User ID & Customer name** | Edit global identity defaults for backups. |
| **? Help** | Short descriptions of each menu action. |
| **Exit** | Close the CLI. |
## Protection modes and size limits
| Mode | File size | Summary |
| ------------------------------------ | ------------------------------------ | ---------------------------------------------------------------- |
| **RSA + passphrase (recommended)** | up to **401 bytes** | Two layers; uploads via secure data; enclave checksum verified. |
| **RSA only** | up to **445 bytes** | One layer; no passphrase. |
| **Envelope encryption + passphrase** | **above 470 bytes**, up to **10 MB** | Passphrase plus DEK envelope; uploads a **ZIP** via secure file. |
The key file must be non-empty. Envelope mode requires files **larger than 470 bytes**.
### Passphrase rules (custom passphrases)
When you choose **Create my own passphrase**, it must be 16–128 characters, use at least three character classes, contain at least 10 distinct characters, and avoid common passwords or obvious patterns. The CLI validates each entry. By default the CLI generates a strong passphrase instead. Either way, you must save the passphrase outside the CLI — CoinCover cannot recover it.
## Supported platforms and release assets
macOS (Intel or Apple Silicon), Linux (x86\_64 or ARM64), or Windows x64. Replace `v0.x.x` with the release tag.
| Asset | Platform |
| ----------------------------------------------------------------- | ------------------- |
| `cc-cli-macos-arm64-v0.x.x` or `cc-cli-macos-arm64-v0.x.x.tar.gz` | macOS Apple Silicon |
| `cc-cli-macos-x64-v0.x.x` or `cc-cli-macos-x64-v0.x.x.tar.gz` | macOS Intel |
| `cc-cli-linux-x64-v0.x.x` or `.tar.gz` | Linux x86\_64 |
| `cc-cli-linux-arm64-v0.x.x` or `.tar.gz` | Linux ARM64 |
| `cc-cli-win-x64-v0.x.x.exe` or `.zip` | Windows x64 |
Tarballs contain an executable named `coincover` at the archive root. Bare binaries (no extension) use the `cc-cli-*` filename — rename to `coincover` when adding to your `PATH`. Each release may include `checksums.txt` (and Cosign signature files on internal releases) for verification.
## Local configuration file
Settings are stored at `~/.coincover/config.json`.
| Stored | Not stored |
| ---------------------------------------------- | ------------------ |
| Active environment | Backup passphrases |
| API keys (per environment) | Private key files |
| Saved User ID and Customer name | |
| Backup receipts (after each successful backup) | |
The file is created with mode `0600` (readable only by your user). It is **not encrypted** on disk — use full-disk encryption on the machine and restrict who can access your account. For stronger protection, future versions may move secrets into the OS keychain.
## Backup receipts
After a **successful** backup upload, the CLI saves a JSON receipt at:
```text theme={null}
~/.coincover/receipts/{ISO-timestamp}-{backup_id}.json
```
The timestamp uses ISO-8601 with colons replaced by hyphens for filesystem safety (e.g. `2026-05-27T11-45-12.123Z-a3f7c2d1-….json`). Each receipt records `backup_id`, `key_id`, `backup_name`, `package_id` (your Backup ID), `environment`, and `created_at` — no passphrases or key material. Files are mode `0600`; the receipts directory is `0700`. The **Backup complete** screen includes the receipt path when saving succeeds.
## Debug file logging
To record a detailed trace of CLI actions (menu choices, API calls, encryption steps, file paths), set:
```bash theme={null}
export CC_LOGS=true
```
Then run the CLI as usual. Logs are appended to:
```text theme={null}
~/.coincover/logs/key-vault-YYYY-MM-DD.log
```
Each line is ISO-timestamped. API keys, passphrases, private keys, and ciphertext payloads are **not** written in full — but logs may still contain file paths, user identifiers, and backup metadata. Treat log files as sensitive; enable only while diagnosing an issue, then disable (`unset CC_LOGS`) and delete old logs when finished.
## Troubleshooting
| Problem | What to try |
| ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **API error** | Read the message; choose **View full error details?** for request/response bodies (long base64 fields may be truncated). Check API key, environment, and network. |
| **Checksum mismatch after upload** | Do not rely on the backup until investigated; contact CoinCover support with the error details. |
| **File too large** | For files above 401 bytes, use **Envelope encryption + passphrase** (up to 10 MB). For small files, RSA only allows up to 445 bytes vs 401 for RSA + passphrase. |
| **Wrong private key on recovery** | RSA decryption usually fails immediately; ensure you use the key from the recovery package for this backup. |
| **Wrong passphrase** | You have three attempts for passphrase-backed backups; passphrase is case-sensitive. |
| **Unrecognised envelope** | File may be corrupt or not from this CLI; wrong key can rarely produce garbage — verify both files. |
| **Recovered file "unsupported format" in Archive Utility** | The recovered file is plaintext with a misleading name if the manifest lacked `original_filename`; rename to the correct extension (e.g. `.jpeg`). |
| **Selected payload.enc instead of ZIP** | Go up one folder and choose `*-backup-package.zip`, not files inside an extracted package. |
| **Permission denied (Linux/macOS)** | Run `chmod +x` on the binary. |
| **macOS blocks the app** | See the Gatekeeper steps in the [Key Vault CLI guide](/business-recovery/cli-integration-guide#install-and-run). |
| **Cancelled backup before review** | No upload occurred; safe to restart. |
| **Declined review (Proceed? No)** | No API calls; nothing changed on CoinCover. |
## Security checklist
* **Passphrases:** store backup passphrases (CLI-generated or your own) in a team vault or password manager, separate from key files and from the encrypted backup blob.
* **API keys:** treat like secrets; do not commit `~/.coincover/config.json` to source control.
* **Environment:** read the **Environment:** line in the CLI before backing up — confirm you are on `prod` (or your intended target), not `dev` or `staging`.
* **Machine:** prefer a dedicated, patched workstation with disk encryption for backup and recovery operations.
* **Recovery files:** handle released private keys and ciphertext with the same care as plaintext keys; delete local copies when your policy allows.
# Overview
Source: https://developer.coincover.com/institutional-recovery/overview
How to integrate CoinCover recovery for your institution — through a supported wallet platform, our API, or the CLI.
Recover for Institutions gives custodians, exchanges, fintechs, and self-custody teams a credible disaster-recovery path: CoinCover holds encrypted recovery material on your behalf and gets you back access to your assets when a key is lost or compromised. Your plaintext key material is never visible to us or our staff.
How you integrate comes down to one question — whether your wallet platform is one we already support.
## On a supported platform?
If you run on a wallet platform we already integrate with, recovery is served **through that platform**. We sit behind the scenes using its native backup and recovery mechanism, so there's typically nothing for you to build.
| Platform | Wallet model | Backup mechanism |
| ---------------------------------------- | ------------------------------ | ------------------------------ |
| [Fireblocks](/partner-guides/fireblocks) | MPC-CMP, 3-of-3 | RSA encryption via Partner API |
| [BitGo](/partner-guides/bitgo) | Multi-sig, 2-of-3 | Key ceremony |
| [Cobo](/partner-guides/cobo) | MPC, 2-of-3 | Key ceremony |
| [Utila](/partner-guides/utila) | MPC, 2-of-2 | RSA encryption via Partner API |
| [Fordefi](/partner-guides/fordefi) | MPC, 2-of-2 | Manual handover (no API) |
| [Copper](/partner-guides/copper) | MPC, 2-of-3 (Copper Unlimited) | Key ceremony |
If your platform is on this list, talk to your account manager — we handle the integration through the platform.
## Not on a supported platform?
If your platform isn't supported, you integrate with CoinCover directly. There are three ways to do it — pick whichever fits how you operate:
Use the API to encrypt key material client-side and store the ciphertext with CoinCover.
Use the API to make CoinCover a key in your multi-sig set up.
Back up and recover from the command line — no integration code required.
We add new platform integrations regularly, so if you'd rather we support your platform directly, talk to us.
## What we always do
Whichever route you take, the protections are the same:
Every key pair is generated inside an AWS Nitro Enclave. The private key is held online for HOT keys, or in our offline, geo-redundant vault for COLD keys. We can't read your recovery material in plaintext, and neither can our staff.
Every recovery is gated by identity verification through CoinCover Control. The right person gets your assets back, and no-one else.
Every assignment, every storage operation, every recovery — recorded and retained for the life of the relationship.
Live support for recovery incidents, whenever they happen.
## What's next
The encrypt-and-store API integration.
Make CoinCover part of your multi-sig or MPC setup.
Building a consumer wallet for end customers instead? See [Recover for Retail](/retail-recovery/overview).
# Testing
Source: https://developer.coincover.com/institutional-recovery/testing
Sandbox is where you build confidence. Use it to walk through every scenario your runbook is meant to cover — including the ones you hope never to hit.
## What sandbox gives you
* A faithful copy of the production API, with the same encryption and the same enclaves
* Lower rate limits to protect shared infrastructure
* Test data that resets on request — talk to us if you want a clean slate
## Scenarios to cover
Here's the test matrix we recommend partners walk through before going live.
### Happy path
Call `POST /v1/partner/keys` against the sandbox base URL. Confirm the key appears in your audit log.
Encrypt a small test payload client-side with the public key (RSA-OAEP, SHA-256), then call `POST /v1/partner/secure/data` with the ciphertext and plaintext checksum.
Call `POST /v1/partner/secure/file` with a small encrypted binary. Confirm you receive a `backup_id`.
Verify each backup packages appear in the CoinCover Control log with the right `customer_id` and `package_id`.
Recoveries run through CoinCover Control. Coordinate with your account manager to walk a sandbox recovery end-to-end before production.
### Failure paths — Partner Key Service
These are the API-side failures most worth rehearsing.
| Scenario | What to test |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Missing or invalid token** | Drop the `Authorization` header, or use an expired token. Expect `401 Unauthorized`. |
| **Insufficient permissions** | Use a token without partner-endpoint scope. Expect `403 Forbidden`. |
| **Duplicate key assignment** | Call `POST /v1/partner/keys` twice with the same `user_identifier` and `organisation`. Expect `409 Conflict` with the message "Key already assigned for this user and organisation". |
| **Bad checksum** | Submit a checksum that isn't a valid SHA-256 hex string. Expect `400 Bad Request` with a field-keyed validation error on `checksum`. |
| **Wrong padding type** | Submit `padding_type: "PKCS1"` or any value other than `OAEP`. Expect `400 Bad Request`. |
| **Oversized file** | Upload a file larger than 10MB to `/v1/partner/secure/file`. Expect `413 Payload Too Large`. |
| **Unsupported file type** | Upload with a `Content-Type` other than `application/octet-stream`. Expect `415 Unsupported Media Type`. |
| **Network failure mid-flight** | Drop the connection during a store operation. Confirm your retry logic reaches a consistent end-state — see [error handling](/operations/error-handling) for the recommended pattern. |
### Failure paths — recovery flow
Recoveries run through CoinCover Control — a human-driven UI, not an API. When a recovery is approved, the recovery material is delivered to the authoriser as a GPG-encrypted secure download. There's no programmatic surface here for partners to integrate against; you're testing the end-to-end operational path with us.
Coordinate with your account manager to walk these scenarios end-to-end in sandbox before production:
* **Authoriser not on the ACL** — the request from an unauthorised identity should be refused
* **Authoriser's GPG key is wrong or missing** — they receive a payload they can't decrypt; confirm your runbook handles this without exposing plaintext
* **Identity verification doesn't complete in time** — the recovery should expire cleanly, not hang
* **Stale key reference** — recovery against a rotated or superseded key should be rejected
### Operational scenarios
| Scenario | What to test |
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Authoriser leaves the company** | Remove from ACL in Control, confirm new requests from them are refused. |
| **Authoriser device lost** | Re-onboard the authoriser. Confirm the old credentials are invalidated. |
| **Rotation drill** | Rotate every key in your sandbox account. Confirm each new assignment lands cleanly and the older keys remain queryable for audit. |
| **HOT vs COLD selection** | Walk through both `key_environment` values for a representative customer. Confirm your downstream tooling handles each correctly — environment is fixed for the life of the key. |
# BitGo
Source: https://developer.coincover.com/partner-guides/bitgo
Backup and recovery across the BitGo self-custody wallet integration pathways — API-created and UI-created wallets.
BitGo has been a CoinCover partner for a long time, and we support backup and recovery for the **BitGo self-custody wallet**. How you integrate depends on two things: how the wallet was created, and which backup mechanism suits that wallet.
There are two ways to create a self-custody wallet — through the **BitGo API**, or through the **BitGo web UI (console)** — and each offers two backup approaches.
Selecting CoinCover in BitGo — or completing any of the backup flows below — does **not** on its own mean your wallets are protected. Backup only takes effect once you have a commercial agreement with CoinCover covering them. If you don't have one in place yet, [contact us](https://www.coincover.com/contact) before relying on any backup.
## At a glance
| Wallet created via | Wallet type | Backup approach | What CoinCover holds |
| ------------------ | ---------------- | --------------------------------------------- | -------------------------------------- |
| API | Multi-sig | Key ceremony — CoinCover is a backup key | The backup private key, held encrypted |
| API | MPC | Encrypt-and-store an RSA-wrapped backup key | The encrypted backup package |
| UI / console | Multi-sig | CoinCover selected as backup key provider | The backup private key, held encrypted |
| UI / console | Multi-sig OR MPC | Self-generated KeyCard, backed up via the CLI | The encrypted KeyCard package |
## Backup
### Wallets created via the BitGo API
When the wallet is created programmatically through the BitGo API, you integrate using the CoinCover API. There are two approaches depending on the wallet type.
#### Multi-sig — CoinCover as a backup key
For multi-sig wallets, CoinCover acts as the **backup key** in your BitGo wallet. You assign a signer key via the CoinCover API (`POST /v1/keys` with `key_type: "secp256k1"`, or `"ed25519"` for chains that use it); CoinCover generates the key pair inside our hardware-isolated key vault and returns the compressed public key. The private key is only ever handled in plaintext inside that vault; at rest it's held encrypted, so neither you nor CoinCover staff can read it.
BitGo — like most multi-sig platforms — expects a BIP32 extended public key (xpub) for each key in the wallet. An xpub is `(compressed public key + 32-byte chain code)` plus BIP32 metadata, and the enclave returns the compressed public key but not a chain code, so there's no native xpub. You supply the chain code, assemble the xpub yourself, and store the chain code back with CoinCover so it's available at recovery. You then register the assembled xpub as the backup key in BitGo's wallet-creation key ceremony.
The worked example is the `secp256k1` / BIP32 case. `ed25519` keys (used by chains such as Solana) are also supported, but they follow their chain's own hierarchical derivation scheme rather than BIP32 — the chain-code and xpub assembly steps don't apply. The assign, store, and recovery shape is otherwise the same.
The full worked flow — assigning the signer key, generating the chain code, assembling the xpub, and registering it in the key ceremony — lives on the Add a backup key page.
#### MPC wallets — encrypt and store a backup key
For MPC wallets, CoinCover can't take part in the key ceremony. Instead, the customer generates their own backup key and stores it with CoinCover as encrypted material:
Create the backup key on your side, following BitGo's process for the wallet.
Assign an `rsa4096` key via the CoinCover API to get an encryption public key.
Encrypt the backup key with the RSA public key (RSA-OAEP, SHA-256), client-side. Your plaintext key never reaches CoinCover.
Store the ciphertext with CoinCover, which holds it securely until recovery.
The encrypt-and-store API flow — assigning the RSA key, encrypting client-side, and storing the ciphertext — is documented in full on the Back up key material page.
### Wallets created via the BitGo UI (console)
When the wallet is created through the BitGo web console, there are two backup approaches.
#### CoinCover selected as the backup key provider
For a set of supported multi-sig assets, BitGo lets you select **CoinCover as the backup key provider** during wallet creation. Behind the scenes, CoinCover acts as the backup key: it provides the public key used in BitGo's wallet-creation key ceremony and holds the corresponding private key encrypted at rest.
As part of onboarding, there's a three-step activation:
Select CoinCover as the backup key holder when you create the wallet in BitGo. This applies to **every wallet you want protected** — CoinCover only holds a backup key for wallets where it has been designated at the point of creation.
Create a BitGo access token that gives **read-only** access to the BitGo wallets CoinCover protects. CoinCover uses it to reconcile your wallets at backup time and confirm the correct ones are backed up. Follow BitGo's instructions to generate the token.
Send the access token to [support@coincover.com](mailto:support@coincover.com), encrypted with the GPG key supplied in your welcome email so it's protected in transit. CoinCover support then validates with you which wallets we hold the backup key for.
This option is available for the multi-sig assets that BitGo supports with CoinCover as a backup key provider.
#### Self-generated KeyCard, backed up via the CoinCover CLI
For assets not natively supported — where CoinCover can't be selected as the backup key provider — you generate the backup key yourself. BitGo issues a **KeyCard PDF** containing the key material, which you then back up with CoinCover:
CoinCover issues you an API key for use with the CLI as part of onboarding.
Use the CoinCover Key Vault CLI to back up the KeyCard PDF. Because the PDF is larger than the small-file limit, it uses the **envelope encryption** pathway — the file is encrypted on your machine and only ciphertext is uploaded.
CoinCover stores the encrypted package securely, with the access controls and governance managed in CoinCover Control.
Installing the CLI and running the envelope-encryption backup flow is covered on the CLI integration guide.
However you back up — CoinCover selected as the backup key, the API flows, or self-generated KeyCards via the CLI — and even across a mix of them, all your backups show up together centrally in CoinCover Control, giving you a single view of what's protected.
## Recovery
The recovery process varies slightly based on the type of backup you have completed. The most common pathway is, CoinCover releasing the material it has been holding, and you complete the recovery using BitGo's recovery tooling.
Raise a recovery request in CoinCover Control. Release is gated by identity verification and your approval policy — the material is only released once those conditions are satisfied. As part of the request you supply a **public GPG key**, which CoinCover uses to encrypt the recovery package in transit.
Depending on the backup pathway, CoinCover returns the backup private key (and, for the multi-sig key-ceremony case, the stored chain code), or the encrypted backup package for you to decrypt locally. The release is encrypted to the GPG public key you supplied, so you first decrypt it with your **GPG private key** before using the material.
Use the released material with BitGo's recovery tooling to sign a transaction that moves the assets to a new wallet you control.
The exact recovery steps differ slightly by pathway — a key-ceremony backup key is reconstructed from the released backup private key plus the stored chain code, while a CLI or encrypt-and-store backup is decrypted locally from the released package.
## What's next
The multi-sig key-ceremony backup flow.
The RSA encrypt-and-store flow for MPC wallets.
The CLI backup flow for self-generated KeyCards.
All CoinCover partner integrations.
# Copper
Source: https://developer.coincover.com/partner-guides/copper
Backup and recovery for the Copper Unlimited MPC wallet — an offline key ceremony, recovery through CoinCover Control, and the self-service Copper Recovery tool.
Copper Unlimited is Copper's MPC wallet, built in a **2-of-3 setup** — any two of three key shards are needed to sign a transaction. CoinCover acts as an independent, trusted third party: we hold a securely encrypted **backup shard** offline, so if you or Copper ever lose access to a shard, you still have the two shards needed to restore access to your Copper Vault.
Backup happens once, during onboarding, in a short **key ceremony** coordinated between you, Copper, and CoinCover. Recovery runs through **CoinCover Control**: anyone on your team can raise a request, but nothing is released until your approvers sign off. Once released, you regain access with Copper's **Copper Recovery** tool.
## At a glance
| Attribute | Detail |
| -------------------- | -------------------------------------------------------------------------------------- |
| Wallet model | MPC, 2-of-3 (Copper Unlimited) |
| Backup mechanism | Offline key ceremony, coordinated between you, Copper, and CoinCover |
| What CoinCover holds | A securely encrypted backup shard, held offline as an independent third party |
| Recovery | Requested and gated through CoinCover Control; completed with the Copper Recovery tool |
## Backup
Backup is a one-time setup, created in a short key ceremony once your Copper agreement and CoinCover Control onboarding are in place. The ceremony itself takes only a few minutes.
Pick a suitable time with Copper and CoinCover for the key ceremony — the short, coordinated session where your backup is created.
In the ceremony, CoinCover creates and securely backs up an encrypted copy of your Copper shard. It takes around ten minutes per key, and CoinCover runs the process with you and Copper — there's no tooling for you to set up.
CoinCover stores your encrypted backup offline in our secure vault and confirms when it's live. From that point, your Copper Vault is protected and ready to recover, and the backup appears in CoinCover Control.
CoinCover only ever holds an **encrypted** backup shard — never enough on its own to move your assets. A recovery always needs your own shard (or Copper's) together with the one CoinCover releases, and it only happens once your approvers in Control have signed off.
## Recovery
Recovery is for an emergency — a lost shard or password, or Copper being unavailable. It's requested and gated through **CoinCover Control**: anyone on your team can raise a request, but nothing is released until your approvers sign off. Once released, you regain access with Copper's Copper Recovery tool.
Raise a recovery request in CoinCover Control and select the backup to recover. As part of the request you supply a **public GPG key**, which CoinCover uses to encrypt the released material so it's protected in transit. New to GPG? See [Create a GPG key pair](/guides/control/create-gpg-key).
The named approvers on the backup are asked to approve, each re-verifying their identity as they vote. Nothing is released until a simple majority has approved.
Because your Copper backup is held offline, CoinCover's team retrieves it before releasing — Control tells you this is happening and notifies you when the encrypted download is ready. You download it and decrypt it locally with your **GPG private key**.
Copper Recovery needs any two of the three shards — the one CoinCover released, together with your own (or one from your Copper Account Manager). Upload both shard files, enter the password protecting each, and the tool generates the private key for your wallet addresses in seconds.
Use the generated private key to move your assets — import it into another wallet app, or use it to sign a transaction out of your Copper Vault.
The step-by-step recovery walkthrough in Control — raising the request, gathering approvals, and collecting the released material.
## What's next
Backup and recovery across the BitGo self-custody wallet, including its key-ceremony flow.
The console-based backup flow and key-return recovery for the Fordefi self-custody MPC wallet.
The workspace and device-share backup flows for the Fireblocks MPC workspace.
All CoinCover partner integrations.
# Fireblocks
Source: https://developer.coincover.com/partner-guides/fireblocks
Backup and recovery across the Fireblocks MPC workspace — the workspace backup and the owner's device key share backup.
Fireblocks uses **MPC-CMP in a 3-of-3 setup**: two of the three key shares live with Fireblocks, and the third lives on the wallet owner's device (in its secure enclave). Every vault and address in a workspace is derived from a single workspace master key (a BIP32 HD wallet), so **one backup covers the entire workspace**.
CoinCover sits alongside Fireblocks as the backup and recovery layer. The integration is **API-based** — CoinCover provides an RSA public key through the Partner API, which is used to encrypt the material that needs protecting. There are **two independent backup flows**, because there are two distinct risks: Fireblocks itself becoming unavailable, and the owner losing their device.
## At a glance
| Backup flow | Protects against | What CoinCover holds |
| ------------------------ | ------------------------------- | ------------------------------------------------------------- |
| Workspace backup | Fireblocks becoming unavailable | The encrypted backup package (`.zip`) and the RSA private key |
| Owner's key share backup | The owner losing their device | The encrypted auto-passphrase and the RSA private key |
In both flows CoinCover holds only encrypted material and the RSA private key — never a share it can use on its own. The RSA private key is held encrypted at rest and only ever handled in plaintext within our hardware-isolated key vault.
## Backup
The integration is API-based: CoinCover generates an RSA-4096 key pair through the Partner API, hands the public key to Fireblocks, and holds the private key. The two flows below both reuse that same key pair.
### Workspace backup
Protects against Fireblocks becoming unavailable. This is a one-time setup at onboarding that covers the whole workspace.
CoinCover generates an RSA key pair via the Partner API. The public key is sent to Fireblocks; the private key is held by CoinCover, encrypted at rest.
Fireblocks encrypts its two key shares with CoinCover's RSA public key.
The workspace owner sets a **recovery passphrase** — known only to them — which encrypts their device key share.
All three encrypted shares are bundled into a single `.zip` and sent to CoinCover for secure storage.
CoinCover holds the backup `.zip` and the RSA private key, but the device share inside the `.zip` is encrypted with the customer's recovery passphrase, which CoinCover never sees. Without that passphrase the third share can't be decrypted, so CoinCover alone can't reconstruct the wallet.
### Owner's key share (device) backup
Protects against the owner losing or breaking their device. It's a separate, per-owner flow, triggered when the CoinCover service activates on that owner's device — but it reuses the workspace RSA public key.
The owner receives an enrolment prompt on their device and confirms it with face verification.
The Fireblocks app generates an **auto-passphrase** — random, internal, and never shown to the user. It's distinct from the workspace recovery passphrase.
The auto-passphrase encrypts the device key share (held by **Fireblocks**), and a copy of the auto-passphrase is itself encrypted with the workspace RSA public key and sent to **CoinCover**.
Separation of concerns: Fireblocks holds the encrypted device share, CoinCover holds the encrypted auto-passphrase that unlocks it, plus the RSA private key needed to decrypt that passphrase. Neither party can recover the share alone.
### Vault sync
The two flows above protect the **keys**. Vault sync protects the **derivation paths** — the record of every vault and address in the workspace and how each is derived, which is what's needed to rebuild the accounts at recovery. It's a one-time link-up, recommended at onboarding, that keeps CoinCover's copy of those derivation paths continuously up to date.
The customer sets up an API user in CoinCover Control and links their Fireblocks workspace to it. Ideally this is done during onboarding.
Using that API access, CoinCover pulls the account's wallet data on a 24-hour cycle, maintaining a current, complete set of the derivation paths for every vault and address in the workspace.
Because Control already holds an up-to-date set of derivation paths, a workspace recovery can return them as a CSV alongside the key material, with no manual export needed.
Vault sync is optional but strongly recommended. Without it, a workspace recovery still returns the keys, but the customer has to export their own vault report from Fireblocks at recovery time or rebuild every account manually. With it in place, the current derivation paths are always ready — so we encourage customers to complete this linkage during onboarding.
## Recovery
There are two recovery flows, matching the two backup flows. Both are initiated in **CoinCover Control** and gated the same way: release only happens after identity verification and your approval policy are satisfied, with risk flags routed to manual review. When you raise the request you also supply a **public GPG key** — CoinCover uses it to encrypt the recovery package so it's protected in transit, and you decrypt it locally with the matching private key.
### Workspace recovery — when Fireblocks is unavailable
Submit a recovery request from the CoinCover Control dashboard.
Release is gated by identity verification and your approval policy before any material is released.
CoinCover returns the backup `.zip` and the RSA private key. If the workspace is synced, the release also includes the **vault sync package** — the current derivation paths for every vault — so the customer has everything needed to rebuild all their accounts. The release is encrypted to the GPG public key you supplied, so you first decrypt it with your **GPG private key** before using the material.
On an air-gapped machine, the customer supplies their recovery passphrase and uses the Fireblocks Recovery Utility to decrypt the shares, reconstitute the workspace key, and move assets to a new wallet provider. The derivation data needed to rebuild every vault comes back as a CSV alongside the key material — from the synced record in Control if vault sync is set up, or from the customer's own exported vault report if it isn't.
The **recovery passphrase** set at onboarding is always the customer's to hold — CoinCover never sees it, so it must be stored safely ahead of time.
Assets across every vault and address in the workspace are recovered in a single operation, because all derivations come from the one master key inside the `.zip`.
### Owner device recovery — when the owner lost their device
The Fireblocks account is placed into recovery mode on the Fireblocks side.
The owner requests recovery via CoinCover Control. The same approval gate applies before anything is released.
CoinCover decrypts the stored auto-passphrase with the RSA private key and returns it to the customer, encrypted to the GPG public key they supplied. The customer decrypts it with their **GPG private key** to obtain the plaintext auto-passphrase.
On a new device, the owner enters the auto-passphrase. The Fireblocks app pulls the encrypted share from Fireblocks servers, decrypts it, and restores it to the new device's secure enclave.
## What's next
The API-based single-backup MPC integration.
The console-based self-custody MPC integration.
Backup and recovery across the BitGo self-custody wallet.
All CoinCover partner integrations.
# Fordefi
Source: https://developer.coincover.com/partner-guides/fordefi
Backup and recovery for the Fordefi self-custody MPC wallet — a console-based backup flow and key-return recovery.
Fordefi is a **self-custody MPC wallet in a 2-of-2 setup**. The integration is **console-based with Slack coordination**: CoinCover issues its RSA public key as a `.pem` file, the customer uploads it in the Fordefi Console, and Fordefi delivers the encrypted recovery package to the customer's designated email address — or the customer downloads it from the console instead. CoinCover holds only the corresponding RSA private key, within our hardware-isolated key vault — never the key shares themselves.
Because CoinCover holds just a decryption key, recovery is a **key return**: CoinCover returns the RSA private key under verified conditions, and the customer decrypts the recovery package Fordefi delivered to them to regain access.
## At a glance
| Attribute | Detail |
| -------------------- | ------------------------------------------------------------------------------------------------------ |
| Wallet model | MPC, 2-of-2 (self-custody) |
| Integration | Console-based, coordinated with Fordefi over Slack; RSA public key handed over as a `.pem` file |
| What CoinCover holds | The RSA private key, stored in an environment of customer choosing |
| Recovery | Key return — CoinCover returns the RSA private key; the customer decrypts the recovery package locally |
## Backup
The backup is driven from the Fordefi Console, with CoinCover and Fordefi confirming the public key over Slack at either end so both sides know exactly which key was used.
CoinCover Tech Ops pull the customer's RSA public key from the CoinCover API as a `.pem` file. CoinCover support provides it to the customer and directs them to initiate backup in the Fordefi Console, then confirms with Fordefi over Slack which public key was provided.
The customer uploads the public key in the Fordefi Console and **designates a recovery email address** to receive the backup. We recommend a dedicated recovery inbox, kept separate from everyday mailboxes — even if it were compromised, the contents are encrypted to CoinCover's public key and can't be used. A customer who would rather not use email can skip this and download the package from the console instead.
The customer validates the public key on their mobile device, then waits for the rest of the admin quorum to approve via mobile.
Fordefi encrypts both key shares with the public key and assembles them into a **recovery package**, which it sends to the designated email address — or makes available to download from the Fordefi Console for customers who opted out of email. Fordefi then confirms back to CoinCover over Slack which public key was used and that the customer has received the material.
Unlike the other partners, the Fordefi recovery package bundles the **wallet derivation paths** in alongside the two encrypted key shares. Because those paths live inside the package, Fordefi generates a **fresh recovery package every time a wallet is added** to the account — sent to the designated inbox, or re-downloadable from the console.
## Recovery
Recovery is a key return. CoinCover releases the RSA private key under verified conditions, and the customer runs Fordefi's recovery tool themselves on an offline machine.
Initiate a recovery request in CoinCover Control. Release is gated by identity verification and your approval policy. As part of the request you supply a **public GPG key**, which CoinCover uses to encrypt the recovery package in transit.
CoinCover releases the RSA private key from our hardware-isolated key vault and returns it to the customer securely, encrypted to the GPG public key they supplied. The customer decrypts it with their **GPG private key** to obtain the RSA private key.
On an offline machine, the customer places the Fordefi recovery tool, the encrypted key material (the `.json` extracted from the backup `.zip`), and the CoinCover-provided private key into a folder, then runs the tool to produce an `output.csv` of the per-vault private keys.
The customer uses the recovered keys to access their assets independently of Fordefi. ECDSA-based chains import into standard wallets; EdDSA-based chains (such as Solana, Sui, Aptos) use Fordefi's dedicated recovery-tool commands to move funds.
## What's next
The API-based single-backup MPC integration.
The workspace and device-share MPC integration.
Backup and recovery across the BitGo self-custody wallet.
All CoinCover partner integrations.
# Partners
Source: https://developer.coincover.com/partner-guides/overview
How CoinCover backup and recovery integrates with the wallet and custody platforms we partner with.
CoinCover integrates with the major institutional wallet and custody platforms so recovery works with the tooling you already use. Each partner has its own security model, so we integrate in the way that fits — through the platform's Partner API, an offline key ceremony, or a self-service backup — while the outcome is always the same: CoinCover holds encrypted recovery material and releases it, under verified conditions, when you need it back.
This section documents each partner integration end to end: how backup works across that platform's wallet types, and how recovery runs when a key is lost or compromised.
## Supported partners
| Partner | Wallet model | Backup mechanism |
| ---------------------------------------- | ------------------------------ | ------------------------------------------- |
| [BitGo](/partner-guides/bitgo) | Multi-sig and MPC | Key ceremony, RSA encrypt-and-store, or CLI |
| [Fireblocks](/partner-guides/fireblocks) | MPC-CMP, 3-of-3 | RSA encryption via Partner API |
| Cobo | MPC, 2-of-3 | Key ceremony |
| [Utila](/partner-guides/utila) | MPC, 2-of-2 | RSA encryption via Partner API |
| [Fordefi](/partner-guides/fordefi) | MPC, 2-of-2 | Console-based, RSA `.pem` handover |
| [Copper](/partner-guides/copper) | MPC, 2-of-3 (Copper Unlimited) | Key ceremony |
## What every partner integration shares
Every key pair is generated inside a hardware-isolated key vault. The private key is held online for HOT keys, or in our offline, geo-redundant vault for COLD keys. We can't read your recovery material in plaintext, and neither can our staff.
Every recovery is gated by identity verification through CoinCover Control. The right person gets the assets back, and no-one else.
We don't replace your wallet UX. We sit alongside it, using each partner's native backup and recovery mechanisms.
Every assignment, every storage operation, every recovery — recorded and retained for the life of the relationship.
## What's next
Backup and recovery across the BitGo self-custody wallet integration pathways.
The workspace backup and the owner's device key share backup for the Fireblocks MPC workspace.
The API-based single-backup flow and key-return recovery for the Utila MPC wallet.
The console-based backup flow and key-return recovery for the Fordefi self-custody MPC wallet.
The offline key-ceremony backup and ACL-approved recovery for the Copper Unlimited MPC wallet.
# Utila
Source: https://developer.coincover.com/partner-guides/utila
Backup and recovery for the Utila MPC wallet — a single API-based backup flow and key-return recovery.
Utila uses **MPC in a 2-of-2 setup**. The integration is **API-based**: Utila pulls CoinCover's RSA public key straight from the CoinCover API, so there's no manual key handover. Both MPC shares end up encrypted under that one CoinCover public key, and CoinCover holds only the corresponding RSA private key — never the shares themselves.
Because CoinCover holds a decryption key rather than a signing share, recovery is a **key return**: CoinCover returns the RSA private key under verified conditions, and the customer decrypts their own backup locally to regain access.
## At a glance
| Attribute | Detail |
| -------------------- | --------------------------------------------------------------------------------- |
| Wallet model | MPC, 2-of-2 |
| Integration | API-based — Utila pulls CoinCover's RSA public key from the CoinCover API |
| What CoinCover holds | The RSA private key, stored in an environment of customer choosing |
| Recovery | Key return — CoinCover returns the RSA private key; the customer decrypts locally |
## Backup
A single flow, driven through the API. The customer approves on their device, and the backup only becomes active once they've been notified it's complete.
Utila retrieves CoinCover's RSA public key from the CoinCover API. Nothing needs to be handed over manually.
The customer confirms they want to back up with this public key.
Utila encrypts the Utila share with the public key. The customer's device receives a notification to approve, and on approval the customer's key share is encrypted with the same CoinCover public key.
Once approved, the encrypted backup package (the key material) is available in the Utila console. The customer downloads it — from the record's three-dot menu — and stores it securely, along with the Utila recovery tooling. This is a **one-time** download, and the backup is only active once it's done. A common pitfall is approving the backup but walking away before completing this step.
When Utila pulls CoinCover's RSA public key, verify the `signature` returned with it against your CoinCover verification key before encrypting anything — and, if you want the signature to also bind the customer or workspace, request it with `sign_with`. See [binding business context into the signature](/wallet-provider-recovery/api-integration-guide#optionally-bind-business-context-into-the-signature) in the API integration guide.
Utila keeps the **wallet data separate** from the backup package. The backup package holds only the encrypted key material, so it's a one-time download; the wallet data is exported separately from the Utila console and should be refreshed **periodically**, since it changes as wallets and addresses are added.
## Recovery
Recovery is a key return. CoinCover releases the RSA private key under verified conditions, and the customer runs the Utila offline tool themselves.
Initiate a recovery request in CoinCover Control. Release is gated by identity verification and your approval policy. As part of the request you supply a **public GPG key**, which CoinCover uses to encrypt the recovery package in transit.
CoinCover retrieves the RSA private key and returns it to the customer securely, encrypted to the GPG public key they supplied. The customer decrypts it with their **GPG private key** to obtain the unsharded RSA `.pem` key.
In the Utila offline tool, the customer uploads their CoinCover backup file, their wallet data file, and the decrypted RSA private key, then selects **Recover**. The tool produces a CSV of the recovered wallet keys.
The customer imports the recovered private keys into any compatible wallet (for example MetaMask for ETH or Electrum for BTC) to regain access to their assets.
## What's next
The console-based self-custody MPC integration.
The workspace and device-share MPC integration.
Backup and recovery across the BitGo self-custody wallet.
All CoinCover partner integrations.
# Integration guide
Source: https://developer.coincover.com/retail-recovery/integration-guide
End-to-end integration of Recover for Retail.
This guide walks you through the full retail recovery integration: identity verification, key generation, client-side encryption, backup storage, and the recovery flow with GPG-encrypted return.
The backup phase links a customer's identity with their encrypted data. While a backup can be initiated before verification is complete, a `verification_id` must be provided to associate the records.
What you actually encrypt and send us depends on your architecture. For wallet providers using a split-key design (the typical retail pattern), this is a recovery-key shard, and the encrypted seed phrase itself stays in your own infrastructure. The API contract is identical — see [the overview](/retail-recovery/overview#what-we-hold-depends-on-your-architecture) for context.
Two things are worth knowing before you start. First, all sensitive payloads are encrypted on-device — we never see plaintext. Second, business logic must be driven by CoinCover webhooks, not by the Identity SDK's UI events. The Identity SDK events are for transitions in your wallet UI; the webhooks are your source of truth.
## Backup workflow
### Step 1 — Initialise verification
Call `POST /v1/verification/start` to generate a `verification_id` (the inquiry ID from identity, our IDV provider). Pass that ID to your front end and use it to initialise the identity SDK. Make sure you've configured a callback URL to receive status events (`approved`, `declined`, and the rest).
**Request**
```json theme={null}
{
"user_identifier": "string (required)"
}
```
**Response**
```json theme={null}
{
"verification_id": "string (Identity inquiry ID)"
}
```
### Step 2 — Generate cryptographic keys
Call `POST /v1/key/generate` to obtain the public key your customer will use for client-side encryption. You'll need the `user_identifier` and the `verification_id` from Step 1.
**Request**
```json theme={null}
{
"user_identifier": "string (required)",
"verification_id": "string (required)"
}
```
**Response**
```json theme={null}
{
"key_id": "string (UUID)",
"public_key": "string (hex-encoded)",
"signature": "string (base64)"
}
```
The key pair is generated inside a CoinCover enclave. The response includes a `signature` over the `public_key`, signed by the enclave that generated the key. During integration, CoinCover issues you a verification key — use it to verify the signature on every key-generate response. A successful verification confirms the public key really came from a legitimate CoinCover enclave; if it fails, treat the response as untrusted and escalate before encrypting any backup material.
Store the `key_id` against your customer record. You'll reference it in your audit logs and customer-support tooling.
#### Optionally, bind business context into the signature
By default the signature covers only the `public_key`. Pass a `sign_with` array on the request and the enclave signs a canonical JSON payload built from the fields you name plus the `public_key` — so a verified signature attests to *which* customer the key was issued for, not just the key bytes. That closes off key-substitution between your customers.
Supported fields: `external_customer_id`, `external_package_id`, `pulled_by_id`, `pulled_by_type`, `user_id`, `key_id`, `key_fingerprint`. Don't list `public_key` — it's always included implicitly, and passing it is rejected. Every field you list must resolve to a non-empty value, or the request returns a `400`; `key_fingerprint` is the exception, since it can resolve after the key is generated.
What each field resolves to on a retail request:
* `external_customer_id` / `external_package_id` — resolved server-side from your partner credentials and the `user_identifier`. Retail requests don't carry an `organisation` or `package` block, so you don't send these.
* `user_id` — CoinCover's internal user id for that `user_identifier`, not the identifier string itself.
* `key_id` — the id of the key being generated (the same `key_id` returned in the response).
* `key_fingerprint` — the SHA-256 (hex) of the enclave-issued public-key bytes.
* `pulled_by_id` / `pulled_by_type` — the credential CoinCover authenticated the call with (`api_key` or `user_credentials`). We set these from your auth token; they aren't read from the request body.
`verification_id` is not bindable. It scopes the call but isn't part of the signed payload, so track which verification session a key belongs to in your own records, against `key_id`.
**Request**
```json theme={null}
{
"user_identifier": "string (required)",
"verification_id": "string (required)",
"sign_with": ["external_customer_id", "user_id", "key_fingerprint"]
}
```
**Response**
When `sign_with` is present, the response carries two extra fields alongside `signature`:
```json theme={null}
{
"key_id": "string (UUID)",
"public_key": "string (hex-encoded)",
"signature": "string (base64)",
"signed_payload": "{\"external_customer_id\":\"acme-wallet\",\"key_fingerprint\":\"9f86d081...\",\"public_key\":\"30820122300d...\",\"user_id\":\"5e9b1c74-3a80-4d2e-b1f6-0c7a9e2d4415\"}",
"signed_fields": ["external_customer_id", "user_id", "key_fingerprint"]
}
```
`signed_payload` is the exact [JCS-canonical](https://www.rfc-editor.org/rfc/rfc8785) JSON string the enclave signed: object keys are sorted, and `public_key` is always included alongside the fields you requested. `signed_fields` echoes the fields you requested, in request order — it doesn't include `public_key`.
Verify the signature over the exact `signed_payload` bytes — don't re-serialise the JSON — then confirm the bound values match the customer you're provisioning. A failed signature, or a bound value you didn't expect, means the response is untrusted: stop and escalate.
A contextual signature is verified with the same verification key CoinCover issued you during integration — **not** with the `public_key` in the response. The signature is base64-encoded ECDSA P-256 in DER format.
Requests are rejected with a `400` if you list `public_key`, name a field outside the supported list, or repeat a field — each field may only appear once.
`sign_with` is optional and additive. Omit it and the response is unchanged — the signature covers the `public_key` alone, verified over the UTF-8 bytes of the hex `public_key` string — so existing integrations keep working.
### Step 3 — Encrypt the data on-device
Before calling the store endpoint, encrypt the sensitive data on the customer's device. This is the step that keeps plaintext out of our infrastructure — get it right and you're done.
Use the `public_key` returned in Step 2.
Convert the hex-encoded public key to PEM or DER, whichever your crypto library expects.
Compute a SHA-256 checksum of the original, unencrypted data. You'll send this alongside the ciphertext.
Encrypt the plaintext using RSA-OAEP with SHA-256 padding.
Base64-encode the resulting ciphertext.
### Step 4 — Store the encrypted backup
Call `POST /v1/backup/store` to persist the data.
**Request**
```json theme={null}
{
"user_identifier": "string (required)",
"verification_id": "string (required)",
"public_key": "string (hex-encoded, required)",
"backup": [
{
"item_key": "string (required, e.g., 'recovery_key_shard' or 'seed_phrase')",
"item_value": "string (base64, encrypted data)",
"item_checksum": "string (SHA-256 hex of original data)"
}
],
"metadata": {
"description": "string (optional)",
"content_type": "string (optional)",
"original_filename": "string (optional)"
}
}
```
**Response**
```json theme={null}
{
"backup_type": "data",
"metadata": {
"stored_at": "string (ISO timestamp)",
"backup_items_count": "integer"
},
"backup": [
{
"item_key": "string",
"backup_id": "string (UUID)",
"checksum": "string",
"data_size": "integer"
}
]
}
```
That's the backup phase done. Your customer now has a record they can recover later.
## Recovery workflow
Recovery is a restricted operation. We only release stored backups when we've seen a successful identity verification.
### Step 1 — Re-verify identity
Call `POST /v1/verification/start` to begin a new recovery-specific session. Monitor the status via your configured webhook, or poll `GET /v1/verification/status` if you need a synchronous read.
**Response (`GET /v1/verification/status`)**
```json theme={null}
{
"verification_id": "string",
"status": "pending | approved | declined | review",
"updated_at": "string (ISO timestamp)"
}
```
### Step 2 — Recover the backup
Once the inquiry status is `approved`, call `POST /v1/backup/recover`. You'll need the `verification_id` from Step 1 and a GPG public key — we use the GPG key to encrypt the recovered data on its way back to you. The orchestrator validates server-side that the verification is approved before releasing any backup material.
The endpoint returns every backup item associated with that user and verification — the response is an array. If the user only ever stored one item, you'll get one entry back; if they stored several (up to three per call), you'll get them all.
**Request**
```json theme={null}
{
"user_identifier": "string (required)",
"verification_id": "string (UUID, required)",
"gpg_public_key": "string (GPG public key block format, required)"
}
```
**Response**
```json theme={null}
{
"backup": [
{
"backup_id": "string (UUID)",
"backup_item_key": "string (e.g., 'seed_phrase')",
"recovery_id": "string (UUID)",
"recovery_package": {
"algorithm": "gpg",
"data": "string (base64, GPG-encrypted ciphertext)"
},
"original_filename": "string (if provided at store time)",
"content_type": "string (if provided at store time)",
"original_size": "string (bytes, if available)",
"metadata": {}
}
]
}
```
Each item in the array is a `BackupRecoverItemResponse`. The `recovery_package` is a unified envelope — for retail, the algorithm is always `gpg` and `data` is the base64-encoded GPG ciphertext. If a particular item failed to recover, the entry will include an `error` field instead of `recovery_package` and `recovery_id`.
We use GPG for the return trip because it gives your customer a clear, well-understood mental model: their device generates a key pair, sends us the public half, and we encrypt the recovered backup to it. The plaintext only exists on the customer's device.
### Step 3 — Rotate and re-back-up
After a recovery, the new recovery code should be backed up via `POST /v1/backup/store` so the protection stays current. The store call requires an approved `verification_id`, so plan to perform the re-back-up on the same recovery session you just used to retrieve the data.
## What's next
Every endpoint, request, and response.
Simulating verification outcomes without real customers.
# Overview
Source: https://developer.coincover.com/retail-recovery/overview
Identity-verified backup and recovery for end users of consumer wallets.
Recover for Retail is built for consumer wallet apps. It gives your customers a way back into their wallet if they lose their device or their seed phrase — without you ever holding their plaintext keys.
You ship the wallet experience your customers already know. We add a recovery path that's identity-verified, end-to-end encrypted, and designed for the real moments when customers need it most.
## What it does
Your customer encrypts the recovery material your architecture defines — typically a recovery-key shard — on-device, then sends only the ciphertext to us.
When a customer needs to recover, they go through identity verification first. We only release the backup once the check is approved.
Backups are RSA-OAEP encrypted on the customer's device. Recovered material comes back GPG-encrypted to a key you control.
Webhook events keep your backend in sync with verification state. No polling, no missed transitions.
Every key we generate is signed inside the enclave. You can optionally bind business context — the customer or user a key was issued for — into that signature, so verifying it proves the key belongs to that exact context rather than only proving it came from CoinCover. See [binding business context into the signature](/retail-recovery/integration-guide#optionally-bind-business-context-into-the-signature).
## Who it's for
You'll typically integrate Recover for Retail if you're building:
* A **consumer wallet app** (mobile or web) where customers manage their own keys
* A **non-custodial product** that needs a credible recovery story to compete with custodial alternatives
* A **fintech onboarding** flow that wants to add wallet protection as a value-add
If you're a wallet provider integrating recovery for institutional customers, see [Recover for Institutions](/institutional-recovery/overview) instead.
## How the integration looks
The integration sits across your backend and your wallet UI. Your customer sees an identity check at the right moments — the rest stays out of their way.
The wallet seed phrase/private key is encrypted with a recovery key, which is then split into two shards. Shard 1 and a hash of Shard 2 are encrypted with a CoinCover public key and stored with CoinCover. Shard 2, a hash of Shard 1, and the encrypted seed phrase are stored with the wallet provider.
The customer will complete a biometric verification check that is link to their backup package stored with CoinCover.
When the customer needs to recover — typically months later, on a new device — they kick off a biometric verification check where we will validate that this is the same person who initially backed up.
Once the verification is approved, we return the decrypted backup package, re-encrypted with a GPG key.
Once the CoinCover shard has been recieved on the customer's device the wallet provider will request the customer to verify the recovery is legitimate which will then trigger the release of shard 2, hash of shard 1 and the encrypted seedphrase/private key to the customer's app.
Your customer's app will recombine the encryption key and then decrypt the encrypted seedphrase/private key. The customer is back in.
We never see plaintext keys, plaintext seed phrases, or any other sensitive material from your customers.
### What we hold depends on your architecture
CoinCover provides a vault primitive — we hold an encrypted blob you send us, and release it back to your customer on a successful identity verification. What that blob represents is your choice, and it depends on how your wallet infrastructure is designed.
For wallet providers integrating Recover for Retail, the dominant pattern for EOA wallets is **split-key recovery**:
* The recovery key that encrypts the customer's seed phrase is split between you and us.
* We hold one shard, encrypted with our public key and decryptable only inside our enclave. You hold the other shard alongside the encrypted seed phrase in your own infrastructure.
* At recovery, the customer's device combines both shards to reconstruct the recovery key, then decrypts the seed phrase locally. Plaintext exists only on the customer's device.
* Neither side alone can reconstruct the seed phrase. Compromise of our infrastructure doesn't compromise customer funds.
## What's next
The full backup and recovery flow, end to end.
Every endpoint, request, and response.
Embedding the Identity SDK in your wallet UI.
Simulating verification outcomes without real customers.
# Testing & sandbox
Source: https://developer.coincover.com/retail-recovery/testing
Simulating verification outcomes without real customers.
Sandbox lets you walk through every state in the inquiry lifecycle without sending test users through a full identity verification. You drive outcomes with a header, watch the webhooks fire, and confirm your backend handles each state correctly.
## Simulating outcomes
In non-production environments, you can drive an inquiry to a specific state by calling `POST /v1/verification/simulate` with the `verification_id` you got back from `POST /v1/verification/start` and the action you want to simulate. The endpoint fires the corresponding webhook event and updates the inquiry status — exactly as a real event from our external provider would.
**Request**
```json theme={null}
{
"verification_id": "string (required)",
"action": "string (required, see table below)"
}
```
### Accepted actions
| Value | Description |
| ------------------------- | ----------------------------------------------- |
| `start_inquiry` | Transitions the inquiry to the `started` state. |
| `complete_inquiry` | Marks the inquiry as completed. |
| `fail_inquiry` | Simulates a failed inquiry attempt. |
| `expire_inquiry` | Simulates inquiry expiration. |
| `mark_for_review_inquiry` | Moves the inquiry to manual review. |
| `approve_inquiry` | Triggers an `approved` outcome. |
| `decline_inquiry` | Triggers a `declined` outcome. |
The endpoint is unavailable in production. Don't reference it from production code paths.
## Recommended test plan
We'd recommend partners walk through this matrix before going live.
### Backup happy path
Call `POST /v1/verification/start`, then `POST /v1/verification/simulate` with `action: "approve_inquiry"`. Confirm you receive an `inquiry.approved` webhook within a few seconds.
Confirm the key and signature are returned, and that the signature verifies against the verification key CoinCover issued you during integration. A failed verification means the response should be treated as untrusted — exercise that path before going live.
Confirm each entry in the `backup` array of the response includes a `backup_id`, and the returned `checksum` matches your local SHA-256 of the plaintext.
### Recovery happy path
A new `verification_id` is required for each recovery.
Confirm the response's `backup` array includes the same `backup_item_key` you stored and a `recovery_package` with `algorithm: "gpg"`.
Decrypt the `recovery_package.data` payload and verify its SHA-256 matches the checksum you stored.
Use the same approved `verification_id` to call `POST /v1/backup/store` and confirm the new backup writes successfully.
### Failure paths
| Scenario | Simulate action | What to confirm |
| -------------------------- | ------------------------- | -------------------------------------------------------------------------- |
| Customer abandons mid-flow | `expire_inquiry` | Your backend handles the timeout gracefully. No partial backup is written. |
| Document fails | `fail_inquiry` | The customer is given a clear message. The backup is not stored. |
| IDV declines | `decline_inquiry` | Backup is rejected. Customer is offered a retry, not a hard wall. |
| Manual review | `mark_for_review_inquiry` | Your UI explains the wait. Customer isn't repeatedly retrying. |
# UI components
Source: https://developer.coincover.com/retail-recovery/ui-components
Embedding the Identity SDK in your wallet app for retail recovery.
The retail recovery flow includes an identity verification step that runs on your customer's device. CoinCover orchestrates the inquiry on the back end; your wallet UI mounts the **Identity SDK** to deliver the verification experience itself.
This page covers how the Identity SDK fits into a CoinCover integration. For the SDK's own contract — install commands, methods, props, lifecycle — Our identity provider documentation is the source of truth.
We have an external IDV provider. There is no CoinCover-supplied identity SDK. You install our Identity SDK in your wallet app and pass it the inquiry ID CoinCover gives you.
## How the pieces fit together
Call `POST /v1/verification/start` with the customer's identifier. CoinCover creates the inquiry with the identity provider and returns a `verification_id` — that's the Identity inquiry ID.
Hand the `verification_id` to your front end and use it to initialise Identity's SDK. The SDK takes over the screen and runs the verification flow on the customer's device.
Facial liveness, document checks, and the rest of the verification stack run inside the Identity SDK. Our identity provider reports the outcome back to CoinCover.
Your backend receives `inquiry.approved`, `inquiry.declined`, or one of the other inquiry events. **Drive your business logic from this webhook**, not from the Identity SDK's UI events.
## What we provide vs our identity provider
| CoinCover provides | Identity provider provides |
| --------------------------------------------------------- | ----------------------------------------------------------- |
| The inquiry orchestration (creating, releasing, auditing) | The SDK that runs on the customer's device |
| The webhook stream that drives your backend | The verification UI itself (liveness, document capture) |
| The encrypted backup storage and recovery logic | The mechanism that initialises the SDK with your inquiry ID |
You only contract with CoinCover. Identity provider sits behind the scenes — billing, commercial terms, and provisioning are all managed through CoinCover.
## Source of truth for state
The Identity SDK fires UI events as the customer moves through the verification screens — these are designed for your wallet's screen transitions, progress indicators, and animations. They're not the source of truth for business state.
If a customer closes the browser mid-flow, those events stop firing. CoinCover's webhook stream is what tells you authoritatively whether the verification approved, declined, or expired. Build your release-the-recovery logic on the webhook, not the SDK event.
## Branding and white-labelling
The Identity SDK supports white-labelling — colours, copy, logo placement — within the verification screens themselves. Configuration is done at the inquiry-template level inside Identity provider; CoinCover provisions a template per partner that you can customise. Talk to your account manager to set this up before you start integration testing.
The handoff to the Identity provider feels like a step inside your wallet rather than a detour to a third party. From the customer's perspective, the verification provider is described abstractly — they don't need to know which IDV provider sits behind the scenes.
## Testing
In sandbox, you can drive verification outcomes with the `x-override-status`[ header](/retail-recovery/testing). The Identity SDK still mounts and renders, but the inquiry resolves to whatever outcome you specify so you can exercise the approve, decline, expire, and review paths without real customers.
## What's next
The full backup and recovery flow.
Driving verification outcomes for development.
Every endpoint, request, and response.
# API integration guide
Source: https://developer.coincover.com/wallet-provider-recovery/api-integration-guide
End-to-end integration of the Partner Key Service for wallet providers.
This guide is for **wallet providers** who integrate via CoinCover's Partner Key Service — the API surface for assigning keys to your customers and storing encrypted recovery material on their behalf. It covers the full happy path: assigning a key, encrypting client-side, storing data and files, and getting ready to ship.
If your integration runs through a key ceremony rather than the API (Cobo, Copper Unlimited, BitGo), the partner-specific page is the one to read. The Partner Key Service powers the API-based partners — Fireblocks, Utila, Fordefi.
Backing up your **own** wallets rather than your customers'?
You want [Recovery for Institutions](/institutional-recovery/overview), which uses the standard endpoints. This guide is for providers holding recovery material on behalf of many downstream customers.
## The shape of an integration
Every Partner Key Service integration follows the same three moves:
You call us with an identifier for your customer. We generate an RSA key pair in our enclaves and return the public key, hex-encoded.
Your customer's recovery material is encrypted with the public key — RSA-OAEP with SHA-256 — before it ever leaves their environment. We never see plaintext.
Send the ciphertext to the secure data or secure file endpoint. We hold it. When recovery is needed, the corresponding private key is retrieved from the enclave to decrypt it.
The plaintext key share, seed, or backup file never crosses the boundary. That's the whole point.
## Step 1 — Authenticate
Authenticate with a Bearer token in the `Authorization` header. CoinCover supports both JWT tokens and long-lived API keys. Your sandbox credentials are in your welcome pack. Partner endpoints require a token with partner-endpoint scope.
```bash theme={null}
export COINCOVER_API_KEY=""
export COINCOVER_BASE_URL="https://service.uat-keys.coincover.com"
```
The full authentication model — scopes, rotation, revocation — is on the [authentication page](/api/authentication).
## Step 2 — Choose HOT or COLD at assignment time
Every key sits in one of two environments. The choice is fixed for the life of the key, so pick deliberately.
| Environment | When to use it |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `HOT` | Online generation and storage. Best for high-frequency operations, and applications that need low-latency key access. |
| `COLD` | Offline generation and storage with enhanced isolation. Best for high value wallets, high-value transactions, and the strictest security requirements. |
On the partner endpoint, `key_environment` is required — there's no default.
## Step 3 — Assign a key to a customer
Call `POST /v1/partner/keys` with an identifier for your customer and your tenant structure. `organisation` and `package` are required so the key is filed against the right downstream customer. We return a stable `key_id` and the public key you'll use to encrypt their data.
```bash curl theme={null}
curl -X POST "$COINCOVER_BASE_URL/v1/partner/keys" \
-H "Authorization: Bearer $COINCOVER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"user_identifier": "alice@acme.io",
"key_type": "rsa4096",
"key_environment": "COLD",
"organisation": {
"customer_id": "org-123",
"customer_name": "Acme Corp"
},
"package": {
"package_id": "pkg-456",
"package_name": "Main Workspace"
}
}'
```
```typescript Node theme={null}
const res = await fetch(`${baseUrl}/v1/partner/keys`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
user_identifier: "alice@acme.io",
key_type: "rsa4096",
key_environment: "COLD",
organisation: { customer_id: "org-123", customer_name: "Acme Corp" },
package: { package_id: "pkg-456", package_name: "Main Workspace" },
}),
});
const { key_id, public_key } = await res.json();
```
```python Python theme={null}
import requests
res = requests.post(
f"{base_url}/v1/partner/keys",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
json={
"user_identifier": "alice@acme.io",
"key_type": "rsa4096",
"key_environment": "COLD",
"organisation": {"customer_id": "org-123", "customer_name": "Acme Corp"},
"package": {"package_id": "pkg-456", "package_name": "Main Workspace"},
},
)
key = res.json()
key_id, public_key = key["key_id"], key["public_key"]
```
Store the `key_id` against your customer record. You'll reference it in your own audit logs and customer-support tooling.
The response also includes a `signature` over the `public_key`, signed by the enclave that generated the key. During integration, CoinCover issues you a verification key — use it to verify the signature on every assign-key response. A successful verification confirms the public key really came from a legitimate CoinCover enclave; if it fails, treat the response as untrusted and escalate before encrypting any recovery material.
### Optionally, bind business context into the signature
By default the signature covers only the `public_key`. If you want it to also attest to *which* customer, package, or user a key was issued for, pass a `sign_with` array on the assign-key request. The enclave signs a canonical JSON payload built from the fields you name plus the `public_key` (which is always included), so a verified signature proves the key is bound to that exact context — which closes off key-substitution across your customers or packages.
Supported fields: `external_customer_id`, `external_package_id`, `pulled_by_id`, `pulled_by_type`, `user_id`, `key_id`, `key_fingerprint`. Don't list `public_key` — it's always included implicitly, and passing it is rejected. Every field you do list must resolve to a value, or the request returns a `400`.
What each field resolves to:
* `external_customer_id` / `external_package_id` — your `organisation.customer_id` / `package.package_id` from the request.
* `user_id` — CoinCover's internal user id for the `user_identifier` you sent (not the identifier string).
* `key_id` — the id of the key being assigned (the same `key_id` returned in the response).
* `key_fingerprint` — the SHA-256 (hex) of the enclave-issued public-key bytes.
* `pulled_by_id` / `pulled_by_type` — the credential CoinCover authenticated the call with (`api_key` or `user_credentials`). CoinCover sets these from your auth token; they aren't read from the request body.
```bash curl theme={null}
curl -X POST "$COINCOVER_BASE_URL/v1/partner/keys" \
-H "Authorization: Bearer $COINCOVER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"user_identifier": "alice@acme.io",
"key_type": "rsa4096",
"key_environment": "COLD",
"organisation": { "customer_id": "org-123", "customer_name": "Acme Corp" },
"package": { "package_id": "pkg-456", "package_name": "Main Workspace" },
"sign_with": ["external_customer_id", "external_package_id", "user_id"]
}'
```
```typescript Node theme={null}
const res = await fetch(`${baseUrl}/v1/partner/keys`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
user_identifier: "alice@acme.io",
key_type: "rsa4096",
key_environment: "COLD",
organisation: { customer_id: "org-123", customer_name: "Acme Corp" },
package: { package_id: "pkg-456", package_name: "Main Workspace" },
sign_with: ["external_customer_id", "external_package_id", "user_id"],
}),
}).then((r) => r.json());
```
When `sign_with` is present, the response carries two extra fields alongside `signature`:
```json theme={null}
{
"key_id": "b1f7c0de-2a4c-4f1e-9c3a-8d2e6b5a1f04",
"public_key": "04a1b2c3d4e5f6789...",
"signature": "MEUCIQD...base64...",
"signed_payload": "{\"external_customer_id\":\"org-123\",\"external_package_id\":\"pkg-456\",\"public_key\":\"04a1b2c3d4e5f6789...\",\"user_id\":\"5e9b1c74-3a80-4d2e-b1f6-0c7a9e2d4415\"}",
"signed_fields": ["external_customer_id", "external_package_id", "user_id"]
}
```
`signed_payload` is the exact [JCS-canonical](https://www.rfc-editor.org/rfc/rfc8785) JSON string the enclave signed: object keys are sorted and the `public_key` is always included alongside the fields you requested. `signed_fields` echoes the fields you requested, in request order (it doesn't include `public_key`).
To verify the binding, check the signature over the exact `signed_payload` bytes with your verification key, then confirm the bound values are the ones you expect:
```typescript Node theme={null}
import crypto from "node:crypto";
// CoinCover provides the verification key (PEM) and its signature scheme during integration.
function verifyBoundKey(res, verificationKeyPem) {
const ok = crypto.verify(
"sha256", // ECDSA P-256 / SHA-256 by default — confirm the scheme with CoinCover
Buffer.from(res.signed_payload, "utf8"), // the exact signed bytes — do not re-serialise
verificationKeyPem,
Buffer.from(res.signature, "base64"),
);
if (!ok) throw new Error("signature invalid — treat the response as untrusted");
const bound = JSON.parse(res.signed_payload);
if (bound.external_customer_id !== "org-123") {
throw new Error("key is not bound to the expected customer");
}
return bound;
}
```
`sign_with` is optional and additive. Omit it and the response is unchanged — the signature covers the `public_key` alone, verified over the UTF-8 bytes of the hex `public_key` string (not its decoded bytes) — so existing integrations need no changes.
The full request and response schema is in the [API reference](/wallet-provider-recovery/api-reference).
## Step 4 — Encrypt client-side
The public key comes back hex-encoded. Convert it to PEM, then encrypt with RSA-OAEP using SHA-256 as both the hash and MGF1 hash.
The checksum must be SHA-256 of the **plaintext**, not the ciphertext.
```typescript Node theme={null}
import crypto from "node:crypto";
function hexToPem(publicKeyHex: string): string {
const der = Buffer.from(publicKeyHex, "hex");
const b64 = der.toString("base64").match(/.{1,64}/g)!.join("\n");
return `-----BEGIN PUBLIC KEY-----\n${b64}\n-----END PUBLIC KEY-----\n`;
}
function encryptForPartner(publicKeyHex: string, plaintext: Buffer) {
const pem = hexToPem(publicKeyHex);
const ciphertext = crypto.publicEncrypt(
{
key: pem,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: "sha256",
},
plaintext,
);
const checksum = crypto.createHash("sha256").update(plaintext).digest("hex");
return { data: ciphertext.toString("base64"), checksum };
}
```
```python Python theme={null}
import base64
import hashlib
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
def hex_to_pem(public_key_hex: str) -> bytes:
der = bytes.fromhex(public_key_hex)
b64 = base64.encodebytes(der).decode().strip()
return f"-----BEGIN PUBLIC KEY-----\n{b64}\n-----END PUBLIC KEY-----\n".encode()
def encrypt_for_partner(public_key_hex: str, plaintext: bytes) -> dict:
pem = hex_to_pem(public_key_hex)
public_key = serialization.load_pem_public_key(pem)
ciphertext = public_key.encrypt(
plaintext,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
),
)
checksum = hashlib.sha256(plaintext).hexdigest()
return {
"data": base64.b64encode(ciphertext).decode(),
"checksum": checksum,
}
```
RSA-OAEP has a maximum message size (around 446 bytes for a 4096-bit key with SHA-256). For anything larger, encrypt the payload with a fresh symmetric key and wrap that symmetric key with the public key — or use the file endpoint, which expects an already-encrypted binary blob.
## Step 5 — Store encrypted data
Send the ciphertext, the plaintext checksum, and the public key to `POST /v1/partner/secure/data`. We use the public key to identify the storage location — there's no need to repeat the environment. On the partner endpoint the checksum is required.
```bash theme={null}
curl -X POST "$COINCOVER_BASE_URL/v1/partner/secure/data" \
-H "Authorization: Bearer $COINCOVER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"public_key": "04a1b2c3d4e5f6789...",
"checksum": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"data": "eyJrZXkiOiJ2YWx1ZSJ9",
"padding_type": "OAEP",
"metadata": {
"description": "Encrypted key shares",
"content_type": "application/json",
"original_filename": "key_shares.json"
}
}'
```
## Step 6 — Upload encrypted files
For binary backups — wallet seed files, encrypted archives — use `POST /v1/partner/secure/file`. This is `multipart/form-data` and expects the file to already be encrypted on your side. CoinCover applies no additional encryption server-side.
```bash theme={null}
curl -X POST "$COINCOVER_BASE_URL/v1/partner/secure/file" \
-H "Authorization: Bearer $COINCOVER_API_KEY" \
-F "public_key=04a1b2c3d4e5f6789..." \
-F "file=@encrypted_backup.bin" \
-F 'metadata={"description":"Wallet seed backup","original_filename":"encrypted_backup.bin"}'
```
The maximum payload size is 10MB. If you need more, talk to your account manager about chunked upload patterns.
## Step 7 — Test before you ship
Run through the [going to production checklist](/operations/going-to-production) before you cut any production traffic to live keys. The most common issues we see at this stage are:
* Hex-to-PEM conversion off by one byte (always verify with a known-good public key first)
* OAEP hash and MGF1 hash mismatched (both should be SHA-256)
* Checksum computed over ciphertext rather than plaintext
* Sandbox base URL still set in production config
The [testing page](/wallet-provider-recovery/testing) has the full sandbox matrix we recommend.
## What's next
Every endpoint, request, and response.
The sandbox matrix to run before production.
# Overview
Source: https://developer.coincover.com/wallet-provider-recovery/overview
Backup and recovery for institutional digital asset wallets, integrated with your wallet platform — for providers backing up their customers' wallets.
Recover for Wallet Providers is built for wallet providers, custodians, and exchanges who want to offer recovery to **their own customers**. We hold encrypted recovery material on your behalf, integrated with the wallet platform you already use, and we get your customers back access to their assets when they need it.
You stay in control of your day-to-day wallet UX. We sit behind the scenes — invisible until recovery is needed.
Backing up your **own** wallets rather than your customers'? You want [Recover for Businesses](/business-recovery/overview), which integrates CoinCover directly over the standard endpoints.
## How we integrate
We integrate with each major institutional wallet platform in the way that fits its security model. Some partners use our Partner Key Service API directly; others run an offline key ceremony with our Custody Operations team. Either way, the outcome is the same — encrypted recovery material held by CoinCover, available to your customers when they need it.
| Partner | Wallet model | Backup mechanism |
| ---------------------------------------- | ------------------------------ | ------------------------------ |
| [Fireblocks](/partner-guides/fireblocks) | MPC-CMP, 3-of-3 | RSA encryption via Partner API |
| [BitGo](/partner-guides/bitgo) | Multi-sig, 2-of-3 | Key ceremony |
| [Cobo](/partner-guides/cobo) | MPC, 2-of-3 | Key ceremony |
| [Utila](/partner-guides/utila) | MPC, 2-of-2 | RSA encryption via Partner API |
| [Fordefi](/partner-guides/fordefi) | MPC, 2-of-2 | Manual handover (no API) |
| [Copper](/partner-guides/copper) | MPC, 2-of-3 (Copper Unlimited) | Key ceremony |
If your wallet platform isn't on the list, talk to us — we add new integrations regularly and the Partner Key Service is designed to be portable.
## What we always do
Whichever partner you use, the integration shape is the same:
Every key pair is generated inside an AWS Nitro Enclave. The corresponding private key is then held in our online environment for HOT keys, or in our offline, geo-redundant vault for COLD keys. We can't read your recovery material in plaintext, and neither can our staff.
Every recovery is gated by identity verification through CoinCover Control. The right person gets their assets back, and no-one else.
We don't replace your wallet UX. We sit alongside it, using each partner's native backup and recovery mechanisms.
Every assignment, every storage operation, every recovery — recorded and retained for the life of the relationship.
## Two integration models
How you integrate depends on your wallet platform.
### Partner Key Service API
Used by Fireblocks, Utila, and Fordefi. CoinCover provides RSA public keys via a REST API. You — or your wallet platform — encrypt recovery material with the public key.
If this is your model, the [API integration guide](/wallet-provider-recovery/api-integration-guide) is your starting point.
### Key ceremony
Used by Cobo, Copper Unlimited and BitGo. CoinCover's Custody Operations team coordinates a key ceremony with your wallet platform and the end customer. The resulting key share is stored on a FIPS 140-2 Level 3 device in a secure vault.
If this is your model, the partner-specific page is the one to read — there's no API integration to do on your side. Onboarding runs through your account manager.
## What every customer gets
Whichever model you're on, every customer ends up with the same protections:
* Encrypted recovery material, with private keys generated inside AWS Nitro Enclaves and stored online (HOT) or in our offline, geo-redundant vault (COLD)
* CoinCover Control for identity-verified recovery requests
* A 24/7 on-call line for live recovery incidents
* Audit logs covering assignment, storage, and recovery for the life of the relationship
## Who it's for
You'll typically integrate Recover for Wallet Providers if you fit one of these descriptions:
* A **wallet provider** (Fireblocks, BitGo, Cobo, Copper, Utila, Fordefi, or similar) offering recovery as a value-add to your customers
* A **custodian** holding institutional digital assets and offering a recovery story to the customers whose assets you hold
* An **exchange or fintech** providing wallet infrastructure to customers and needing a fallback path if a key is lost or compromised
If you're backing up your own wallets rather than your customers', see [Recover for Businesses](/business-recovery/overview). If you're building a consumer wallet for end customers, you probably want [Recover for Retail](/retail-recovery/overview).
## What's next
For partners using the Partner Key Service.
Every Partner Key Service endpoint, request, and response.
# Testing & sandbox
Source: https://developer.coincover.com/wallet-provider-recovery/testing
Walking through every recovery scenario before you ship — for wallet providers on the Partner Key Service.
Sandbox is where you build confidence. Use it to walk through every scenario your runbook is meant to cover — including the ones you hope never to hit.
## What sandbox gives you
* A faithful copy of the production API, with the same encryption and the same enclaves
* Lower rate limits to protect shared infrastructure
* Test data that resets on request — talk to us if you want a clean slate
## Scenarios to cover
Here's the test matrix we recommend partners walk through before going live.
### Happy path
Call `POST /v1/partner/keys` against the sandbox base URL. Confirm the key appears in your audit log.
Encrypt a small test payload client-side with the public key (RSA-OAEP, SHA-256), then call `POST /v1/partner/secure/data` with the ciphertext and plaintext checksum.
Call `POST /v1/partner/secure/file` with a small encrypted binary. Confirm you receive a `backup_id`.
Verify each backup package appears in the CoinCover Control log with the right `customer_id` and `package_id`.
Recoveries run through CoinCover Control. Coordinate with your account manager to walk a sandbox recovery end-to-end before production.
### Failure paths — Partner Key Service
These are the API-side failures most worth rehearsing.
| Scenario | What to test |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Missing or invalid token** | Drop the `Authorization` header, or use an expired token. Expect `401 Unauthorized`. |
| **Insufficient permissions** | Use a token without partner-endpoint scope. Expect `403 Forbidden`. |
| **Duplicate key assignment** | Call `POST /v1/partner/keys` twice with the same `user_identifier` and `organisation`. Expect `409 Conflict` with the message "Key already assigned for this user and organisation". |
| **Bad checksum** | Submit a checksum that isn't a valid SHA-256 hex string. Expect `400 Bad Request` with a field-keyed validation error on `checksum`. |
| **Wrong padding type** | Submit `padding_type: "PKCS1"` or any value other than `OAEP`. Expect `400 Bad Request`. |
| **Oversized file** | Upload a file larger than 10MB to `/v1/partner/secure/file`. Expect `413 Payload Too Large`. |
| **Unsupported file type** | Upload with a `Content-Type` other than `application/octet-stream`. Expect `415 Unsupported Media Type`. |
| **Network failure mid-flight** | Drop the connection during a store operation. Confirm your retry logic reaches a consistent end-state — see [error handling](/operations/error-handling) for the recommended pattern. |
### Failure paths — recovery flow
Recoveries run through CoinCover Control — a human-driven UI, not an API. When a recovery is approved, the recovery material is delivered to the authoriser as a GPG-encrypted secure download. There's no programmatic surface here for partners to integrate against; you're testing the end-to-end operational path with us.
Coordinate with your account manager to walk these scenarios end-to-end in sandbox before production:
* **Authoriser not on the ACL** — the request from an unauthorised identity should be refused
* **Authoriser's GPG key is wrong or missing** — they receive a payload they can't decrypt; confirm your runbook handles this without exposing plaintext
* **Identity verification doesn't complete in time** — the recovery should expire cleanly, not hang
* **Stale key reference** — recovery against a rotated or superseded key should be rejected
### Operational scenarios
| Scenario | What to test |
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| **Authoriser leaves the company** | Remove from ACL in Control, confirm new requests from them are refused. |
| **Authoriser device lost** | Re-onboard the authoriser. Confirm the old credentials are invalidated. |
| **Rotation drill** | Rotate every key in your sandbox account. Confirm each new assignment lands cleanly and the older keys remain queryable for audit. |