Custody API

Signing & EnQlave

Qustody never holds signing keys. When a transaction clears policy and screening it parks in PENDING_SIGNATURE and waits for a signature produced by your signer — the EnQlave desktop app, or a service you build against the contract below. Signatures use Quantum Chain's quantum-safe signature scheme.

Key & signature formats

ItemSizeEncoding in API payloads
Public key1952 byteshex string (3904 hex chars)
Signature3293 byteshex string (6586 hex chars)
Signing digest32 byteshex, Keccak-256 of the unsigned transaction
Address20 bytes0x-hex — last 20 bytes of Keccak-256(publicKey)

Length checks are exact and hard: any other size is rejected. Always submit unprefixed lowercase hex for signatures and public keys — the wallet-registration endpoint tolerates a 0x prefix, the signature-submission endpoint does not.

Option A — EnQlave (recommended)

EnQlave is the desktop signer for Linux and Windows. It generates and stores Qeys (signing keys) locally, registers wallets with the ownership proof automatically, displays every pending transaction for review, and submits signatures — you never touch the wire format. Verify downloads against the published SHA256SUMS.

Option B — your own signer (pull flow)

1. tx reaches PENDING_SIGNATURE 2. GET /v1/transactions/{id}/signing_payload ── fetch the digest 3. sign the 32-byte digest locally, self-verify 4. POST /v1/transactions/{id}/signature ── submit {signature, publicKey} 5. Qustody verifies, assembles, re-verifies, broadcasts

GET/v1/transactions/{id}/signing_payload

{
  "transactionId": "…",
  "signingHash": "ab34…",          // 32-byte digest, hex, no 0x — this is what you sign
  "sourceAddress": "…",            // source wallet ID
  "destinationAddress": "0x…",
  "assetId": "QC",
  "sourceVaultId": "…",
  "amount": "1000000000000000000", // present for transfers
  "note": "…", "metadata": {…}
}
10-minute TTL Signing payloads expire after 10 minutes. A signature submitted after expiry fails the transaction with signing payload expired — re-create the transaction rather than caching digests.

POST/v1/transactions/{id}/signature

{
  "signature": "<hex, 3293 bytes>",   // required, no 0x prefix
  "publicKey": "<hex, 1952 bytes>"    // required, no 0x prefix
}

Verification rules, in order:

  1. The transaction must currently be PENDING_SIGNATURE (else 409 INVALID_TX_STATE, code 1301).
  2. The payload must not be expired.
  3. publicKey must exactly match the wallet's registered key — a valid signature from any other key returns 403 SIGNATURE_MISMATCH (code 1302).
  4. The signature must verify over the stored 32-byte digest.

On success the transaction moves SIGNED → BROADCASTING → CONFIRMING.

Fail-closed broadcast assembly

Before broadcast, Qustody rebuilds the unsigned transaction from the stored intent only, requires the rebuilt hash to equal the digest you signed, then re-verifies the assembled raw transaction field by field. Neither a compromised database record nor a tampered payload can redirect funds — the signature simply won't match, and the transaction fails.

Option C — hosted signer endpoint (push flow)

Instead of polling, Qustody can call your signer over HTTPS when a signature is needed (30-second timeout). Your endpoint receives:

{
  "requestId": "…",
  "signingHash": "<base64>",        // 32-byte digest — base64 in the push flow
  "sourceAddress": "0x…",
  "destinationAddress": "0x…",       // present for transfers
  "amount": "1000000000000000000",
  "metadata": {"vaultId": "…", "assetId": "QC", "policy_version": "…"}
}

And must respond with one of:

// sign immediately (200)
{"signature": "<hex>", "publicKey": "<hex>", "status": "signed"}

// defer for human approval (202) — submit later via POST …/signature
{"status": "pending_approval", "approvalId": "…"}

// refuse (200)
{"status": "rejected"}

If your signer enforces its own policy versions, a policy_version mismatch is retried once with a recomputed version.

Registering keys

Whichever signer you run, each wallet's public key must be registered up front — either generated via …/wallets/generate or registered with the ownership-proof challenge Keccak256("qustody:register:" + lowercase(address)). Details in Vaults & Wallets. Signature submissions are only accepted from the registered key.