Skip to main content
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, 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:
1

Assign a key

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.
2

Encrypt client-side

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.
3

Store the ciphertext

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.
export COINCOVER_API_KEY="<your-jwt-or-api-key>"
export COINCOVER_BASE_URL="https://service.uat-keys.coincover.com"
The full authentication model — scopes, rotation, revocation — is on the authentication page.

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.
EnvironmentWhen to use it
HOTOnline generation and storage. Best for high-frequency operations, and applications that need low-latency key access.
COLDOffline 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.
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"
    }
  }'
Store the key_id against your customer record. You’ll reference it in your own audit logs and customer-support tooling. The response also includes a signature over the public_key, signed by the enclave that generated the key. During integration, CoinCover issues you a verification key — use it to verify the signature on every assign-key response. A successful verification confirms the public key really came from a legitimate CoinCover enclave; if it fails, treat the response as untrusted and escalate before encrypting any recovery material. The full request and response schema is in the 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.
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 };
}
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.
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.
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 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 has the full sandbox matrix we recommend.

What’s next

API reference

Every endpoint, request, and response.

Testing & sandbox

The sandbox matrix to run before production.