Skip to main content
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.

Step 1 — Authenticate

Authenticate with a Bearer token in the Authorization header — a JWT or a long-lived API key.
export COINCOVER_API_KEY="<your-jwt-or-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.
EnvironmentWhen to use it
HOTOnline generation and storage. Best for high-frequency operations and low-latency access.
COLDOffline 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.
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"
  }'
Store the key_id against your wallet record. The response also carries a signature over the public_key, signed by the generating enclave — verify it with your CoinCover verification key before relying on the key. If verification fails, treat the response as untrusted and escalate.

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.
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 };
}
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.
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.
# Confirm the key's shards reconstruct and the pair is valid
curl "$COINCOVER_BASE_URL/v1/hot/keys/$KEY_ID/verify" \
  -H "Authorization: Bearer $COINCOVER_API_KEY"

# Confirm a stored backup is valid and recoverable
curl "$COINCOVER_BASE_URL/v1/hot/backups/$BACKUP_ID/verify" \
  -H "Authorization: Bearer $COINCOVER_API_KEY"
verify-key returns is_key_valid; verify-backup returns is_backup_valid. Treat a false from either as a hard failure and escalate before relying on the material.

Step 7 — Test before you ship

Run through the going to production checklist before cutting production traffic to live keys. The most common issues:
  • Hex-to-PEM conversion off by one byte (verify with a known-good public key first)
  • OAEP hash and MGF1 hash mismatched (both should be SHA-256)
  • Checksum computed over ciphertext rather than plaintext
  • Sandbox base URL still set in production config
  • key_environment defaulted to HOT where you intended COLD
The testing page has the full sandbox matrix.

What’s next

Add CoinCover as a backup key

Use a secp256k1 key as the backup key in your multi-sig or MPC wallet.

API reference

Every endpoint, request, and response.