# 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**
* **Invitation-only signup** — Signing up to Control is now restricted to people who have received an invitation from an administrator, or who are completing an administrator-initiated reset. Nobody can self-register against your organisation.
* **Access control lists** — Access control lists are now first-class objects you can manage in their own right, rather than being tied to a single backup. New list and detail pages show every list, who is on it, and which backups it governs — and a single list can now cover multiple backups. Approval requests link straight to the list they affect, so approvers can review a change in context.
* **Recovery policy review and attestation** — Recovery policies now go through a documented review and attestation step. Policies carry version history and are superseded cleanly when replaced, and member tasks and Test Recovery unlock once the policy has been attested.
* **Test Recovery drills** — Owners can run a Test Recovery drill end to end, including a one-time download, to prove a recovery would work before it is ever needed. Control now checks the required prerequisites are in place before a drill can start, so you find gaps up front instead of mid-drill.
* **Recovery Readiness training** — Readiness training can now be assigned and tracked for the people named in your recovery plan, with progress visible alongside their other readiness tasks.
* **Resend pending invitations** — You can now resend an invitation to any user who hasn't yet completed signup, straight from the system users list. Previously the only option was to remove the user and invite them again.
* **Security keys for approvals** — The security key you register for sign-in now also works for organisation-level approval prompts, so you can use one method throughout.
**Bug fixes**
* Fixed an error when creating an API key that prevented some administrators from completing the action.
* Pending access control change requests are now only shown to genuine members of the affected list. Previously they could also appear for users who held broad read permissions but no membership.
* Approval notifications for an access control change now reach every member of the relevant list, including where that list covers several backups.
* A recovery can no longer be started while a change to the governing access control list is still awaiting approval.
* Fixed several issues on the access control screens: the total count no longer changes when you filter, the backups table refreshes immediately after you assign a list, and you can now undo the outgoing side of a pending member replacement.
* Fixed issues in the Recovery Readiness setup flow, where some steps were unclear and certain capabilities were incorrectly shown as unavailable to eligible organisations.
* Uploading a recovery policy now works reliably from Control.
* The organisation switcher now names the organisation you are currently working in.
**Security**
* Applied the outstanding dependency advisories across the platform. No customer action is required.
**New features**
* **Sign in with a YubiKey** — You can now use a YubiKey or other WebAuthn security key as your multi-factor method at sign-in, as an alternative to an authenticator app. You choose which method to use when you are prompted.
* **Organisation API keys** — Organisations can now create and manage their own API keys, so you can integrate Control with your own systems.
* **Recovery Readiness (early access)** — First release of Recovery Readiness. Owners can begin setting up their readiness programme and assign tasks to members. Availability is controlled per organisation — speak to your account contact if you would like it enabled.
**Improvements**
* The vault list is now paginated and usable on accounts with a large number of vaults.
**Bug fixes**
* Vault sync is significantly more reliable. Syncs no longer get stuck in a pending state on large accounts, no longer fail part-way through when a custody provider connection expires, and correctly resume on a first-time sync. Large vault sets now sync completely rather than stopping short.
* Fixed an issue where preparing a hot-key-only recovery download would fail.
* Fixed an issue where members could not complete multi-factor setup if sign-in MFA was not enabled for their organisation.
* You can now remove a partner from an organisation when it has no backups assigned to it.
* Clearer error messages when assigning a user to an organisation or sending an invitation fails.
**Security**
* Applied the outstanding high and medium severity dependency advisories across the platform. No customer action is required.
**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
Create an Access Control List, choose the approval policy that decides its votes, and give members the roles that fit how your organisation approves recoveries.
An Access Control List (ACL) is the group of named people authorised to approve the recovery of the backups it protects, and to approve any change to who is on the list. You decide how many of them must agree by choosing an **approval policy** when you create the ACL. The members can vote to change it later.
Changing who's on an ACL, what role each member has, or its approval policy isn't an administrative action. The people already on the list vote on it, under the ACL's current policy. That's what ensures nobody outside the list, whether one of your own administrators or CoinCover, can quietly change who is able to release your material.
## At a glance
| | |
| - | - |
| **Scope** | One ACL can protect many backups. Each backup is protected by one ACL. |
| **Approval policy** | **Majority**, **Percentage** or **Exact count**, chosen when you create the ACL. Changing it later needs a vote. |
| **Member roles** | **Normal**, **Veto** or **Must approve**. A member can be both Veto and Must approve. |
| **Minimum size** | One member, as long as the policy can always be met by the members you add |
| **Who creates an ACL** | An Organisation Owner |
| **Who can propose a change** | An Organisation Owner or a member of the ACL, with **Edit ACL** |
| **Who approves a change or a recovery** | The ACL's members, under its current policy. Members being removed vote too. |
| **What doesn't need a vote** | Renaming the ACL, and attaching more backups to it. Only an Organisation Owner can do either. |
| **Voting window** | Set for your organisation. Seven days by default. |
| **Verification of Vote** | Each member's identity is verified at the point of voting, with an optional hardware security key for added assurance. |
## Approval policies
The policy sets the **threshold**: how many approvals a vote needs. It applies to every vote the ACL holds, both recovery requests for its backups and changes to its own membership.
| Policy | How the threshold is worked out | 3 members | 4 members | 5 members |
| - | - | - | - | - |
| **Majority** | More than half of the members | 2 | 3 | 3 |
| **Percentage** (example: 60%) | That share of the members, always rounded up | 2 | 3 | 3 |
| **Exact count** (example: 2) | The number you set, however many members there are | 2 | 2 | 2 |
Some worked examples:
* **Majority with 4 members** needs 3 approvals. Half of 4 is 2, and the policy needs more than half.
* **Percentage at 51% with 4 members** needs 3 approvals. 51% of 4 is 2.04, which rounds up to 3.
* **Percentage at 75% with 6 members** needs 5 approvals. 75% of 6 is 4.5, which rounds up to 5.
* **Percentage at 10% with 5 members** needs 1 approval. Any percentage always needs at least one, and Control warns you when your percentage rounds down to a one-person rule.
* **Exact count of 2 with 5 members** needs 2 approvals. If the ACL later grows to 8 members, it still needs 2.
Majority and Percentage thresholds follow the ACL's current size. If members are added or removed later, the number of approvals needed changes with them. An Exact count threshold stays the same.
## Member roles
Each member has one of these roles, chosen as you add them:
| Role | What it means |
| - | - |
| **Normal** | Their vote counts toward the threshold like everyone else's. |
| **Veto** | Their rejection declines the request on its own, whatever anyone else has voted. Their approval counts toward the threshold like a Normal member's. |
| **Must approve** | The request can't be approved without their approval, even when the threshold is met. Their approval also counts toward the threshold. |
A member can be both **Veto** and **Must approve**. Their rejection declines the request straight away, and nothing passes without their approval.
### How the outcome is decided
Control checks a vote in this order every time someone votes:
1. **Voting window.** If the window has closed, the request expires and nothing is applied, whatever votes came in.
2. **Veto.** If any Veto member has rejected, the request is declined straight away. This applies even if a Must-approve member has approved.
3. **Threshold.** If approvals reach the threshold, the request is approved, subject to step 4. If rejections reach the threshold, it's declined. If everyone has voted and neither side reached it, it's declined.
4. **Must approve.** If the threshold is met but a Must-approve member hasn't approved yet, the request stays open for them. If a Must-approve member rejected, the request can't pass, and it's declined once everyone has voted or when the window closes.
A vote only counts once that member's verification has been completed.
#### Worked example
An ACL of five members uses **Majority**, so it needs 3 approvals. Alex is **Must approve** and Bea is **Veto**. Chris, Dana and Eli are **Normal**.
| What happens | Outcome |
| - | - |
| Chris, Dana and Eli approve | Threshold met, but Alex hasn't approved. The request stays open. |
| ...then Alex approves | **Approved** |
| Alex and Chris approve, then Bea rejects | **Declined** straight away. Bea vetoed it. |
| Alex rejects; Chris, Dana and Eli approve | Can't pass without Alex. **Declined** once Bea has voted too, or when the window closes. |
| Chris and Dana reject | Still open: 2 rejections is below the threshold of 3. |
| Chris, Dana and Eli reject | **Declined**. Rejections reached the threshold. |
## Create an ACL
Here is the whole setup, from inviting approvers to the first vote. Every step is done by your organisation. CoinCover doesn't take part in creating an ACL.
```mermaid theme={null}
flowchart TD
A["Owner: invite approvers from the Team page"] --> B["Approvers: finish account setup and identity verification"]
B --> C["Owner: Access Control Lists, then Create ACL"]
C --> D["Select Policy Type: Majority, Percentage or Exact count"]
D --> E["Assign ACL Members: set the threshold and give each member Normal, Veto or Must"]
E --> F{"Can the rule ever pass?"}
F -- "No: Control blocks you" --> E
F -- Yes --> G["Confirm Your ACL: read the rule in plain English"]
G --> H["Assign Backups (optional)"]
H --> I["Complete: name the ACL, review, then Create ACL"]
I --> J["The ACL is live and its members are active straight away"]
J --> K["Every later change to members, roles or policy, and every recovery, goes to a vote"]
```
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, and members need a completed identity check before they can vote.
As an Organisation Owner, go to **Access Control Lists** and select **Create ACL**.
On **Select Policy Type**, choose **Majority**, **Percentage** or **Exact count**. See [Approval policies](#approval-policies) for how each one works.
On **Assign ACL Members**, set the threshold if your policy needs one: the percentage for **Percentage**, or the number of approvals for **Exact count**. Then add each member by choosing their role: **Normal**, **Veto** or **Must**. Choosing a role adds the person. Selecting **Normal** again on a Normal member takes them off the list.
The panel beside the list shows **The rule, in plain English**, for example "At least 3 of 5 members must approve", and names each Veto and Must-approve member. You can't continue while the rule could never pass. For example, an Exact count of 4 with only 3 members is blocked.
On **Confirm Your ACL**, check the rule reads the way you intend, then select **Confirm & continue**. Control points out anything worth a second look, such as Must-approve members who already decide every outcome on their own, or a percentage that rounds down to a single approval.
On **Assign Backups**, choose the backups this ACL protects. Recovering any of them will need this policy. Backups that already have an ACL show as **Already assigned** and can't be selected. This step is optional: you can assign backups later.
On **Complete**, check the name under **Name this ACL**. Control fills in a name that describes the policy, such as "Majority ACL, 5 members". You can change it, but you can't leave it blank. Review the members and backups, then select **Create ACL**. The members you added are active straight away. No vote is needed to set up the list.
## Change an ACL
Once the ACL exists, you change it with **Edit ACL** on the ACL's page. In one go you can add and remove members, change members' roles, change the approval policy, and attach more backups. Everything except the backups goes to a single vote of the current members, under the ACL's current policy.
```mermaid theme={null}
flowchart TD
A["Owner or ACL member: open the ACL, then Edit ACL"] --> B["Members & Roles: add or remove members, change roles or the policy"]
B --> C{"Could the ACL left after the change still pass a vote?"}
C -- "No: Control blocks you" --> B
C -- Yes --> D["Review Vote: check the changes and who votes"]
D --> E["Owner only: Attach Backups, applied straight away without a vote"]
E --> F["Submit for vote"]
F --> G["Every current member votes, including anyone being removed"]
G --> H{"Passes under the current policy?"}
H -- Yes --> I["All the changes apply at once"]
H -- "No, or the voting window closes" --> J["Nothing changes"]
```
Open the ACL from **Access Control Lists** and select **Edit ACL**. An Organisation Owner or any member of the ACL can do this. If **Edit ACL** isn't available, its tooltip says why, for example because a vote is already open or a recovery is in progress on one of the ACL's backups.
On **Members & Roles**, search for someone to add and pick their role, remove members, or change a member's role between **Normal**, **Veto** and **Must approve**. You can also change the policy type or its threshold. Each row shows whether that person is being added, being removed, or having their role changed.
As when you create an ACL, you can't continue if the ACL left after the change could never pass a vote. For example, you can't set an Exact count of 4 while only 3 members would remain.
On **Review Vote**, check **Your changes** and the before and after view of the policy. The page lists who votes and the rule the vote passes under. If someone being removed is a **Veto** or **Must approve** member, Control points out that they can block their own removal.
On **Attach Backups**, choose any more backups this ACL should protect. Backups are attached as soon as you submit, without a vote. A backup that's already protected by another ACL can't be moved to this one, and a backup can't be taken off an ACL once it's attached.
Select **Submit for vote**. Everyone currently on the ACL is asked to approve or reject the change as a whole. If you only attached backups, nothing goes to a vote.
If the vote passes under the ACL's current policy, all the changes apply together. If it's declined, or the voting window closes first, nothing changes. A new policy applies to votes after the change, not to the vote that approves it. See [Approve or reject an ACL change](/guides/control/approve-list-change) for the voter's side.
### Who votes on a change
Every current member votes, including anyone the change would remove. People being added don't vote, because they aren't members yet.
* A **Veto** member can veto their own removal.
* A **Must approve** member has to approve their own removal, or the change can't pass.
The same applies to a change that takes away their Veto or Must approve role.
For example, on a **Majority** ACL of four members (3 approvals needed), a change that removes one member is voted on by all four, and still needs 3 approvals.
### Rename an ACL
Organisation Owners can rename an ACL at any time. On the ACL's page, select its name, enter the new name in **Rename ACL** and save. Renaming doesn't go to a vote, and it doesn't change who can approve anything.
## Good to know
* **One change at a time.** A change can include many edits, but only one can be open for voting on an ACL. Wait for it to finish, or cancel it, before making another.
* **ACLs are frozen during a recovery.** An ACL can't be edited while a recovery is running on one of its backups, 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 change to an ACL.
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 a change to an Access Control List (ACL), such as adding or removing members, changing a member's role or changing the approval policy, everyone already on that ACL is asked to vote. That includes anyone the change would remove. The ACL's current approval policy decides the outcome: **Majority**, **Percentage** or **Exact count**, plus any **Veto** or **Must approve** roles. 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.
Under **Changes being voted on** you'll see every change in the request, with the ACL as it is now and as it will be after the change. A single request can add and remove members, change roles and change the policy. You approve or reject all of it together. 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.
Select **Approve** or **Reject**, then **Confirm**. Your vote counts toward the ACL's threshold. Once enough members approve under the current policy, all the changes take effect at once, and anyone removed loses access to the ACL. If enough reject, or a Veto member rejects, it's declined and nothing changes.
## How your vote is counted
* **The threshold applies both ways.** The change is approved when approvals reach the ACL's threshold, and declined when rejections reach it. If everyone has voted and neither side reached it, the change is declined.
* **If you're a Veto member**, your rejection declines the change on its own. A veto only counts while the vote is still open, so vote promptly.
* **If you're a Must-approve member**, the change can't pass without your approval, even when the threshold is met. It waits for you.
* **If the change alters the policy**, this vote still passes under the current policy. The new policy applies from the next vote.
* **Your vote counts once your identity check completes.**
* **The voting window.** The vote stays open for your organisation's voting window, seven days by default. If it isn't decided by then, it expires and nothing changes.
See [How the outcome is decided](/guides/control/approval-control-list#how-the-outcome-is-decided) for the full order and a worked example.
Your vote is final once you confirm it — it can't be changed afterwards. Review the change carefully before confirming.
Everyone currently on the ACL votes, including anyone the change would remove. If you're being removed, you still vote: as a Veto member you can veto your own removal, and as a Must-approve member your removal can't pass without your approval. People being added don't vote, because they aren't members yet.
## 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. Nothing is released until the ACL's approval policy is met. 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 ACL's threshold, or reject to vote against the recovery. If you're a **Veto** member, your rejection declines it on its own. If you're a **Must approve** member, the recovery can't be approved without you.
Your vote is final once you confirm it. Review the request carefully before confirming.
## What happens next
Once the ACL's approval policy is met, the recovery is authorised and the encrypted package is prepared for the requester to download. If rejections reach the threshold, or a Veto member rejects, the recovery is declined. If a Must-approve member rejects, the recovery can't be approved, and it's declined once everyone has voted. A recovery that isn't decided within your organisation's voting window, seven days by default, expires. Only members of that backup's ACL can approve its recovery.
See [How the outcome is decided](/guides/control/approval-control-list#how-the-outcome-is-decided) for the full rules.
## 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.
# Check a backup with Assurance
Source: https://developer.coincover.com/guides/control/check-a-backup
Run a check that a hot backup is intact and its key can be rebuilt, and keep a signed record of every check.
Assurance lets you check, from Control, that a hot backup CoinCover holds for you is intact and that its key can be rebuilt. Every check is kept in the backup's history, along with records signed by CoinCover, so you can show when a backup was last checked and what the result was.
## At a glance
| | |
| - | - |
| **Where** | A backup's page, on the **Assurance** tab. Each item under **Storage information** on the **Overview** tab also shows its latest result. |
| **Who can run a check** | The organisation Owner, and members of the backup's Access Control List (ACL) for the backups that ACL covers. |
| **Which backups** | Hot backups only. Cold backups are held offline and aren't checked in Control. |
| **When checks run** | When someone selects **Run check now**. One check covers every item on the backup that can be checked. |
| **What you keep** | A history of every check, who started it, and a record of each result signed by CoinCover that you can download. |
## What a check does
Each check runs inside CoinCover's secure enclave. Your key is rebuilt and your backup is decrypted only inside the enclave, and the check returns a result, not the contents.
| Check | What we do | Result when it passes |
| - | - | - |
| **Key check** | Rebuild the backup's key from a random set of its stored parts, and confirm it matches the key you were given. | **Key rebuilt** |
| **File check** | For each stored file or data item, decrypt it inside the enclave and confirm its checksum matches the one recorded when you stored it. | **File unchanged** |
A passed check confirms the backup was intact, and its key could be rebuilt, at the time of the check.
If CoinCover doesn't hold a stored copy of an item, that item shows **Not covered by checks**.
## Run a check
In Control, go to **Backups** and open the backup.
Select **Assurance**, next to **Overview**. The dot on the tab shows the backup's current state.
Select **Run check now**. The button reads **Checking…** while the checks run, and a message tells you the result.
The summary shows the overall result. **What we check** shows the result for the key and for each stored item.
On the **Overview** tab, each item under **Storage information** shows its latest result. Select it to open the **Assurance** tab. If nothing has been checked yet, you can also select **Run check now** there.
### When you can't run a check
**Run check now** is unavailable, and Control shows the reason next to it, when:
* the backup is cold: **Cold backups are held offline and can't be checked.**
* there's nothing on the backup CoinCover can check: **This backup has nothing we can check.**
* a change to the backup's ACL is waiting for votes: **Checks can't run while an access control change is in progress.**
* a recovery of the backup is in progress: **Checks can't run while a recovery is active.**
## What the results mean
| Result | What it means | What to do |
| - | - | - |
| **All checks passed** | Every item that can be checked passed its latest check. | Nothing. |
| **Needs attention** | A check finished and found a problem: the key couldn't be rebuilt, or a file doesn't match the one you gave us. | Contact CoinCover support. A failed check doesn't change or remove your backup. |
| **Didn't finish** | The check stopped because of a problem on CoinCover's side. It tells you nothing about your backup. | Try again in a few minutes. If it keeps happening, contact support. |
| **Not checked yet** | No check has run on this item. | Select **Run check now**. |
| **Held offline** | The backup is cold. | Nothing. Cold backups aren't checked in Control. |
If a check runs but can't reach a result, the summary says so. Run the check again to get one.
## History and signed records
**History** lists every check on the backup with its **Date**, **Result** and who it was **Started by**. Checks started together appear as one row.
Select **Details** on a row to see the technical details for that check: the key parts used, the threshold, the raw outcome, the checksum compared with its baseline, and the signing key.
Select **Download signed records** to save the records for that check. If a check didn't produce a signed record, its details say so. Each record is signed by CoinCover's enclave signing key, so you can check it yourself against CoinCover's published signing keys. See [Validating key signatures](/guides/validating-key-signatures).
Records are signed by CoinCover, so they aren't an independent audit. Control shows the signing key but doesn't check the signature for you.
Each check also appears in the backup's event history.
## If you don't see the Assurance tab
* You need to be the organisation Owner, or a member of the backup's ACL.
* The backup needs at least one item CoinCover can check.
* Assurance needs to be turned on for your organisation. Contact your CoinCover account team.
## Checking backups through the API
If you integrate through the API, you can run the same checks with the `verify` endpoints. See [Back up key material](/institutional-recovery/back-up-key-material#step-6-verify-the-key-and-backup).
## What's next
Get your backed-up key material released when you need it.
Decide who can approve a backup's recovery, and who can check it.
# 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 — how your organisation signs 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: nobody outside the approvers you name — not an administrator, and not CoinCover — can release your material.
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.
## How your organisation signs in
CoinCover sets up sign-in for each organisation. There are three parts to it: how users sign in, which second factor they use, and whether sensitive actions need a hardware security key.
### Sign-in method
| Method | How it works |
| - | - |
| **Password** | Users sign in with their email address and a password. This is the default. |
| **Magic link** | Users enter their email address and we send them a one-time sign-in link. The link expires after 15 minutes and works once. Password sign-in is turned off. |
| **Password and magic link** | Users choose either option on the sign-in page. |
| **Single sign-on (SSO)** | Users sign in through your identity provider. You can make SSO the only way in, or keep passwords as a fallback. |
SSO uses OpenID Connect (OIDC) and is set up for Okta. If you use a different OIDC identity provider, talk to us. SAML isn't supported, and magic links can't be combined with SSO.
* **You send us** your identity provider's issuer URL, plus the client ID and client secret of the app you create for Control.
* **We send you** the redirect URI to add to that app. We can also give you a sign-in URL if you want a Control tile on your Okta dashboard.
* **People still need an invitation.** SSO doesn't create Control accounts on its own. Someone can sign in with SSO only after they've been invited to Control.
* **Roles are managed in Control.** Groups and claims in your identity provider aren't mapped to Control roles.
* **Your identity provider must send a verified email address.** It has to match the email address the person was invited with.
* **MFA happens in your identity provider.** Control doesn't ask SSO users for a second factor at sign-in, so enforce MFA in your identity provider. A security key requirement for sensitive actions (below) still applies.
### Two-factor authentication
Every user who signs in with a password or magic link sets up a second factor the first time they sign in. Two types are available:
| Second factor | How it works |
| - | - |
| **Authenticator app** | A 6-digit code from an app such as Google Authenticator, Microsoft Authenticator, Authy or 1Password. |
| **Security key** | A hardware security key such as a YubiKey, or a passkey stored on the user's device. |
By default, users choose either one. We can limit your organisation to authenticator apps only, or to security keys only. A change applies to new set-ups; methods that users have already set up keep working. SMS codes aren't supported.
### Security keys for sensitive actions
We can also require your users to confirm sensitive actions with a security key, such as voting on a recovery request or on a change to an Access Control List. Users register their keys the first time they sign in after this is turned on. See [Set up your security keys](/guides/control/security-keys).
To change how your organisation signs in, set up SSO, or require security keys, contact your CoinCover account manager or [support@coincover.com](mailto:support@coincover.com).
## 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.
Choose how you'll confirm it's you each time you sign in. You only see the options your organisation allows.
* **Authenticator app:** scan the on-screen QR code with your authenticator app, or enter the key manually, then enter the 6-digit code it generates.
* **Security key:** insert your key and tap it when your browser asks, or use a passkey on your device. See [Set up your security keys](/guides/control/security-keys).
Keep your second factor safe. If you use security keys, register a backup key as well. If you lose access to every method you've set up, 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.
If your organisation requires security keys for sensitive actions, Control asks you to register them before you continue. By default, that's two keys: one you use day to day and a backup. See [Set up your security keys](/guides/control/security-keys).
**If your organisation uses SSO**, you don't create a password, verify your email with a code, or set up a second factor in Control. Enter the email address your invitation was sent to, select **Continue with Okta**, and follow your provider's prompts. You still complete identity verification, and register security keys if your organisation requires them.
## Signing in
Enter your email address first. Control then shows the sign-in options your organisation uses: **Sign in** with a password, **Send magic link**, or **Continue with Okta**. After a password or magic link, confirm it's you with your authenticator app or security key. If you've set up both, you can choose either.
## 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.
Register, manage and replace the keys you use to sign in and approve.
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 the named approvers on the backup's Access Control List — never automatically, and only when the list's approval policy is met. 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 Access Control List with at least one member, and no change to that ACL can be waiting for a vote. 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 the ACL's approval policy must be met. 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.
# Set up your security keys
Source: https://developer.coincover.com/guides/control/security-keys
Register the hardware security keys or passkeys you use to sign in to CoinCover Control and confirm sensitive actions, and what to do if you lose one.
A security key proves it's really you by asking for a tap on a physical device. In CoinCover Control, your keys can do two jobs:
* **Sign-in:** as your second factor after your password or magic link.
* **Sensitive actions:** if your organisation requires it, you confirm actions such as voting on a recovery request with a tap of your key.
You register a key once and Control uses it for both.
## What you can use
| Type | Examples |
| - | - |
| **Hardware security key** | A FIDO2 key such as a YubiKey, connected by USB or NFC. |
| **Passkey** | A passkey stored on your device or in your password manager. You unlock it with Touch ID or Face ID on a Mac or iPhone, Windows Hello on a PC, or your password manager. |
Your organisation may limit this, for example to hardware keys only, or to particular models such as YubiKeys. Control tells you if a key isn't accepted. You need an up-to-date browser such as Chrome, Edge, Safari or Firefox.
New to hardware keys? Your key's manufacturer covers device set-up, such as creating a PIN. For YubiKeys, see [Yubico's set-up guide](https://www.yubico.com/setup/). Your PIN is stored on the key, never on our servers.
## Before you start
If your organisation requires security keys, have **two** ready before you sign in — one for daily use and a backup — unless CoinCover has agreed a different number with your organisation. Keep the backup somewhere safe and separate from your main key.
## Register your keys
There are two places you might be asked to register a key.
### When you set up two-factor authentication
On your first sign-in, Control asks you to **Set up two-factor authentication**.
Select **Security key**. You only see the options your organisation allows.
Insert your key, or hold it to your phone for NFC, and tap it when your browser asks. To use a passkey instead, follow your browser's prompt to create one, and unlock it with Touch ID, Face ID, Windows Hello or your password manager.
Once the key is registered, sign in again and use it to confirm it's you.
### When your organisation requires keys
If your organisation requires security keys for sensitive actions, Control shows **Let's secure your account** until you've registered enough keys. It takes about three minutes.
Give the key a name you'll recognise, such as *Office YubiKey 5C*. Insert it and tap it when your browser asks.
Remove the first key, then do the same with your backup key.
When you see **You're all set**, select **Go to dashboard**.
## Confirm a sensitive action
When your organisation requires security keys, Control asks you to **Verify your identity** before:
* voting to approve or reject a recovery request, or cancelling that vote
* voting on a change to an Access Control List, or cancelling that vote
* removing a security key
Select **Authorise** and tap your key. You won't be asked again for the next 30 minutes, unless your organisation has set a different time.
## Manage your keys
Open the profile menu at the top right, select **Profile**, then **Security**. Under **Security keys** you can see each key, when it was registered and when it was last used.
* **Add a key:** select **Add security key** and follow the prompts.
* **Remove a key:** you can't remove your only second factor, so set up an authenticator app or another key first. If your organisation requires a number of keys, register a replacement before removing one.
A key marked *Not accepted by this organisation's security key policy* doesn't meet your organisation's rules. Register one that does.
## If you lose a key
Use your backup key, or your authenticator app if you've set one up.
In **Profile** → **Security**, select **Add security key** and register the new one.
Remove the lost key from the list so it can no longer be used.
If you've lost every key and don't have an authenticator app, or you've already registered as many keys as your organisation allows, contact [support@coincover.com](mailto:support@coincover.com). We reset your two-factor authentication and you register your keys again.
## Troubleshooting
Update your browser, or switch to a current version of Chrome, Edge, Safari or Firefox.
Your organisation may only allow hardware keys, or particular models. Try a different key, or ask your Organisation Owner which keys are allowed.
If you belong to more than one organisation, a key has to meet every organisation's rules. Contact CoinCover support and we'll help you find a key that works for all of them.
Each request to tap your key lasts five minutes. Start again and tap your key when your browser asks.
## For Organisation Owners
CoinCover enables security keys to be added to your organisation. Contact your account manager or [support@coincover.com](mailto:support@coincover.com) to:
* require a security key as the second factor at sign-in
* require a security key to confirm sensitive actions
* set how many keys each person registers (two by default) and the maximum they can hold (three by default)
* allow hardware keys only, or particular models such as YubiKeys
* change how long a confirmation lasts (30 minutes by default)
If your organisation uses SSO, sign-in MFA is handled by your identity provider, but you can still require security keys for sensitive actions.
# Validating key signatures
Source: https://developer.coincover.com/guides/validating-key-signatures
How CoinCover attests key assignment, and how to verify those signatures against our published JWKS.
CoinCover cryptographically signs every key we generate. Verifying that signature confirms two things: the public key came from a CoinCover enclave, and, when requested, that it is bound to a specific customer, user, wallet or workspace.
If verification fails, treat the response as untrusted and escalate. Do not encrypt recovery material, assemble a wallet, or persist the key as canonical.
## How it works
The generating enclave signs with CoinCover's private signing key. You verify with the matching public key, which we publish as a JSON Web Key Set (JWKS).
The signature is **ECDSA over P-256 with SHA-256** (`ES256`). It is **base64-encoded DER**, not a compact JWS. Verify it with a standard ECDSA verifier; do not treat it as a JWT.
What the signature covers depends on the request:
| Request | Signed bytes |
| - | - |
| Default (no `sign_with`) | The UTF-8 bytes of the hex `public_key` string — not the decoded key bytes |
| With `sign_with` | The exact UTF-8 bytes of `signed_payload`, a [JCS-canonical](https://www.rfc-editor.org/rfc/rfc8785) JSON string |
`signed_payload` always includes `public_key` alongside the fields you named. Object keys are sorted. Do not re-serialise the JSON before verifying — use the string we returned.
## JWKS endpoints
CoinCover publishes a JWKS per environment. Use the document that matches the API you are calling — production signatures will not verify against the UAT keys, and the reverse is also true.
| Environment | Typical API hosts | JWKS |
| - | - | - |
| Production | `https://service.keys.coincover.com`
`https://orchestrator.control.coincover.com` | [https://policies.keys.coincover.com/.well-known/jwks.json](https://policies.keys.coincover.com/.well-known/jwks.json) |
| UAT | `https://service.uat-keys.coincover.com`
`https://orchestrator.uat-control.coincover.com` | [https://policies.uat-keys.coincover.com/.well-known/jwks.json](https://policies.uat-keys.coincover.com/.well-known/jwks.json) |
UAT is the non-production environment these guides use for integration. Develop against it when you want to verify signatures and exercise backup flows without initiating production recoveries. It is the environment the rest of this site currently calls sandbox, and the UAT name is due to change to sandbox — until that rename ships, the UAT hosts above are the ones to use.
Fetch the JWKS over HTTPS and do not follow redirects. Cache production and UAT documents separately: they currently share the `kid` `enclave-signing-key-v1` but the key material is different, so a cache keyed only on `kid` will mix environments.
### Response format
The shape is the same in every environment. This example is production; UAT publishes different `x` and `y` values (and can share the same `kid` string).
```json theme={null}
{
"keys": [
{
"kty": "EC",
"crv": "P-256",
"alg": "ES256",
"use": "sig",
"kid": "enclave-signing-key-v1",
"x": "VJXGyVxF7URJ1ekrziZrtap_pkQ-R-lcjot9T_RLKPM",
"y": "7kOQGa-87OvVVV6_L4OSvz9O-Rg2a07a7HRtJqPPX0M",
"coincover:purposes": ["assignment-sig", "policy-sig"],
"coincover:validFrom": "2026-06-23T00:00:00Z"
}
]
}
```
| Field | Description |
| - | - |
| `kty` | Key type. Assignment signatures use `EC`. |
| `crv` | Curve. Assignment signatures use `P-256`. |
| `alg` | Algorithm. Assignment signatures use `ES256` (ECDSA with SHA-256). |
| `use` | Key usage. Always `sig` for signature verification. |
| `kid` | Key ID in the JWKS document. Use it when caching entries, not as a field on the key-assignment response. |
| `x` / `y` | EC public key coordinates (Base64URL-encoded). |
| `coincover:purposes` | What this key is allowed to sign. For key attestation, require `assignment-sig`. |
| `coincover:validFrom` | Instant from which the key may be used. Ignore keys that are not yet valid. |
Only accept a JWKS entry that is `EC` / `P-256` / `ES256` / `use: sig`, includes `assignment-sig` in `coincover:purposes`, and has a `validFrom` in the past. Other purposes (for example `policy-sig`) are not valid for key-assignment signatures unless they also list `assignment-sig`.
## Verifying a signature
Fetch the JWKS for the environment you called, or use a cached copy (see [Caching JWKS](#caching-jwks)). Keep only keys that match the assignment-signing constraints above.
Use the eligible assignment-signing keys from that JWKS. The key-assignment response does not include a `kid`. Never verify with the `public_key` in the response — that is the recovery key, not CoinCover's attestation key.
Decode `signature` from standard Base64 to raw bytes.
If `signed_payload` is present, verify over those exact UTF-8 bytes. Otherwise verify over the UTF-8 bytes of the hex `public_key` string.
Verify ECDSA P-256 / SHA-256 over those bytes with the JWKS public key. Libraries that distinguish encodings should be told the signature is **DER**, not IEEE P1363.
If verification fails, reject the response. Do not retry with a reconstructed payload.
A valid signature proves the enclave attested those exact bytes. It does not by itself prove the bound customer is the one you expected — do that next.
## Checking the binding
When you sent `sign_with`, also check that the attested values match the assignment you just made:
* `signed_fields` is exactly the list you requested, in that order.
* The parsed `signed_payload` includes `public_key` matching the response.
* Any other bound field you asked for (`key_id`, `user_id`, `external_customer_id`, and so on) matches the assignment you just made.
* If you bound `key_fingerprint`, it equals the SHA-256 of the public-key **bytes** (hex-decode `public_key`, then hash), encoded as **lowercase hex** — not the SHA-256 of the hex string, and not uppercase hex.
A valid signature over unexpected values is still untrusted. Stop and escalate.
How to request the extra fields is covered in each product's integration guide: [wallet providers](/wallet-provider-recovery/api-integration-guide#optionally-bind-business-context-into-the-signature), [institutions](/institutional-recovery/back-up-key-material#optionally-bind-business-context-into-the-signature), and [retail](/retail-recovery/integration-guide#optionally-bind-business-context-into-the-signature).
## Caching JWKS
Each JWKS endpoint returns `Cache-Control: max-age=600, must-revalidate` (ten minutes). Cache the document for that environment, indexed by the `kid` on each JWKS entry, rather than refetching on every key assignment. Keep a separate cache (or a composite key of environment + `kid`) for UAT and production.
During rotation the document can contain more than one key. Because the assignment response does not carry a `kid`, verify against the eligible assignment-signing keys in the cached set. If none of them verify, refresh the JWKS for that environment and retry once as the signing key may have rotated. Do not pin a single key forever.
Respect `max-age`. A cache that never expires will miss rotation; a cache that never stores will add latency and a runtime dependency on the JWKS host for every assignment.
## Common pitfalls
| Issue | What to do |
| - | - |
| Re-serialising `signed_payload` | Verify over the exact string we returned. Re-stringifying JSON changes key order and whitespace and will fail. |
| Verifying with the recovery `public_key` | Use the JWKS attestation key, not the key in the assignment response. |
| Hashing the hex string for `key_fingerprint` | Hex-decode `public_key` first, then SHA-256 the bytes. The digest is lowercase hex. |
| IEEE P1363 signature encoding | The signature is DER-encoded ECDSA. Configure your verifier accordingly. |
| Stale JWKS cache | If none of the cached keys verify, refresh the JWKS for that environment and retry once. |
| Wrong JWKS for the environment | Verify UAT signatures with the UAT JWKS, production signatures with the production JWKS. The `kid` values can match across environments; the key material does not. |
| Using a key with the wrong purpose | Require `assignment-sig` in `coincover:purposes`. |
| Following redirects on the JWKS URL | Fetch the published URL directly over HTTPS. |
# 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.
Welcome to CoinCover
Lost keys should never mean lost assets. CoinCover Recover backs up sensitive key material without ever holding plaintext keys, because everything is encrypted before it reaches us. When you need it back, strict verification makes sure your keys are released to you and no one else, and a full audit trail gives you the evidence to show regulators and clients exactly what happened and when.
Trusted by 600+ financial institutions. 22 million wallets protected. 100% recovery success rate.
Three products, one job. Pick your route and start building.
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.
Request and response schemas, authentication, error codes and webhook
payloads, with example calls you can copy straight into your client.
Want to see what's changed recently? The changelog tab tracks new features, fixes, and improvements.
# 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 CoinCover's published JWKS before continuing — see [Validating key signatures](/guides/validating-key-signatures). 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, as lowercase hex, 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 and the `signed_payload`/`signed_fields` response are in [Back up key material](/institutional-recovery/back-up-key-material#optionally-bind-business-context-into-the-signature) — the mechanics are identical here. Verify the signature as described in [Validating key signatures](/guides/validating-key-signatures).
## 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 against CoinCover's published JWKS before relying on the key. See [Validating key signatures](/guides/validating-key-signatures). 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 of the public-key bytes, as lowercase hex; `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 CoinCover's published JWKS over those exact bytes — don't re-serialise the JSON — then confirm the bound values match the wallet you're provisioning. See [Validating key signatures](/guides/validating-key-signatures). 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.
These checks run on `HOT` keys and their backups only. A `COLD` key returns `400`.
If your organisation uses CoinCover Control, your team can also run these checks there, with a signed record and history for every check. See [Check a backup with Assurance](/guides/control/check-a-backup).
## 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 four-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 user dedicated to CoinCover monitoring rather than using a personal login. Tying CoinCover's access to a service account you control means it outlives any individual, survives people joining or leaving, and never exposes wallets beyond the ones you assign.
1. Create a new BitGo user specifically for CoinCover monitoring.
2. On a BitGo Enterprise account, add the user to the enterprise and complete any approvals needed before it can be added to wallets.
3. Add this user as a **Viewer** on every wallet CoinCover protects, completing any approvals those actions require.
4. Log in as the dedicated user — the access token in the next step is created under this user.
CoinCover only sees wallets this user is a Viewer on, so you stay in control of what's shared. A token created under a personal login instead would inherit that person's wallet access and break if their account were ever changed or removed.
Logged in as the dedicated user, create the access token that gives CoinCover **read-only** access to the protected wallets. CoinCover uses it to reconcile your wallets at backup time and confirm the correct ones are backed up.
In the BitGo console, open **Account Settings → Developer Options** and add an access token with these settings:
* **Label** — `Coincover Monitoring Token`
* **Permissions** — set to **View**
Add CoinCover's five monitoring IPs to the token's IP address whitelist, comma-separated with no spaces (the commas between addresses are required):
```text theme={null}
18.133.68.226,3.11.149.135,3.11.127.247,18.169.123.165,18.169.110.69
```
Check **I agree** and click **Add Access Token**. BitGo then prompts your 2FA device to authenticate the token creation.
BitGo requires an IP whitelist for long-lived production access tokens, so this token only works from CoinCover's monitoring IPs. BitGo's own reference for this flow is [Create Access Tokens](https://developers.bitgo.com/guides/get-started/access-tokens).
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 the backup's approval policy has been met.
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.
## Partners we integrate with today
| 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 |
**Don't see your platform?** We support every non-custodial platform today.
[Talk to us](https://www.coincover.com/contact) and we'll show you how.
## 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 CoinCover's published JWKS before encrypting anything — see [Validating key signatures](/guides/validating-key-signatures). 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. Verify it against CoinCover's published JWKS on every key-generate response — see [Validating key signatures](/guides/validating-key-signatures). 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 of the enclave-issued public-key bytes, as lowercase hex.
* `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. The full verification process, including how to load CoinCover's JWKS, is in [Validating key signatures](/guides/validating-key-signatures).
A contextual signature is verified with CoinCover's published JWKS attestation key — **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 [Validating key signatures](/guides/validating-key-signatures) and [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 CoinCover's published JWKS — see [Validating key signatures](/guides/validating-key-signatures). 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. Verify it against CoinCover's published JWKS on every assign-key response — see [Validating key signatures](/guides/validating-key-signatures). 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 of the enclave-issued public-key bytes, as lowercase hex.
* `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 against CoinCover's published JWKS, then confirm the bound values are the ones you expect. See [Validating key signatures](/guides/validating-key-signatures) for the verification process.
`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. |