Skip to main content

Guveno Wallet SDK for Python

Add secure crypto custody to your platform — create and manage multi-chain wallets, automate withdrawals, and receive real-time webhooks for Bitcoin, Ethereum, XRP, Polkadot, and more. Built for exchanges, fintechs, and platforms.

A typed Python SDK with client-side key encryption and a fully offline signing path, so the server never holds plaintext key material. Fully wire- and key-compatible with the Node.js SDK: the same account derives the same addresses, and secrets sealed by one SDK open in the other.

Prefer the terminal? The @guveno/cli package wraps the same API with the same features.

Install

pip install guveno

Requires Python 3.10+.

How it works

There are two credentials, and they do different jobs:

  • An API key (gv_live_...) authenticates every request. Generate one in the Guveno dashboard. It carries your company and an explicit set of allowed actions, granted per wallet (plus a couple of global actions). A request for a wallet or action the key wasn't granted is rejected.
  • Your encryption password decrypts your recovery phrases. The server only ever stores them sealed to your account's encryption key; the password (set during dashboard onboarding) unlocks that key locally and never leaves your process.

Wallets, addresses, and the sealed secret all live on the server. Listing and reading metadata needs only the API key. Signing (withdrawing, deriving addresses, revealing a phrase) additionally needs your encryption password, because that's what decrypts the key.

Everything is organised as Vault → Wallet → Address:

  • A vault is one recovery phrase: the custody boundary. The members who hold a sealed copy of the phrase (key access) are set on the vault, and that is what decides who can sign.
  • A wallet is one chain and network inside a vault, at one account-level derivation path (derivation_path_prefix, for example m/44'/60'/0'/0). Every wallet in a vault derives from the same phrase and inherits the vault's team. A vault holds one wallet per chain, network, and path; a second Ethereum wallet in the same vault needs a different path.
  • An address is one receiving address of a wallet, at <prefix>/<account_index> (<prefix><account_index> on Polkadot).

Creating a wallet from a new phrase creates its vault. Adding more chains to that vault reuses the phrase, so one backup covers all of them.

Quick start

import os
from guveno import Guveno

guveno = Guveno(api_key=os.environ["GUVENO_API_KEY"])

# Browse — metadata only, no password needed.
wallets = guveno.list_wallets()

# Load a wallet to sign with it: fetches the sealed secret and unlocks it locally.
wallet = guveno.load_wallet(wallets[0].id, os.environ["GUVENO_ENCRYPTION_PASSWORD"])

# Derive and register the next receive address (uses the server-tracked index).
result = wallet.derive_address(label="deposits")
print("Deposit to", result.address["address"])

withdrawal = wallet.withdraw(
    # Omit address_id to auto-select the source: the server picks an address with
    # enough balance — and for Bitcoin aggregates UTXOs across the wallet.
    asset_id=1,
    to_address="0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
    amount="1.5",
)
print(withdrawal.status)  # 'broadcast' — confirmation arrives via webhooks

wallet.lock()  # wipe key material from memory when done

load_wallet() accepts a numeric id, a wallet name, or a {"name", "chain", "network"} selector:

guveno.load_wallet(wallets[0].id, password)   # by id
guveno.load_wallet("treasury", password)      # by name
guveno.load_wallet({"name": "treasury", "chain": "bitcoin", "network": "mainnet"}, password)

Names are only unique within a (chain, network), so a bare name that matches wallets on more than one chain/network raises — pass the scoped selector or the id to disambiguate.

withdraw() runs the full prepare → sign → broadcast loop: the server returns an unsigned payload, the SDK signs it in process, and broadcasts the signed transaction. The recovery phrase never leaves your machine.

By default the source is auto-selected. Pass address_id to send from one specific address, or (Bitcoin only) source_address_ids to restrict UTXO aggregation to a chosen subset of the wallet's addresses. Account-based chains can't combine balances across addresses, so an auto withdrawal raises if no single address covers the amount.

Fees use the server's suggestion by default. Override per call with ethereum_gas (an EthereumGasOverrides) or bitcoin_fee_rate (sat/vB). Bitcoin inputs are fixed at prepare time, so a fee rate much higher than suggested can fail to fit the selected inputs.

Creating and importing wallets

# Generate a new recovery phrase, derive the first address, seal it, and register it.
created = guveno.create_wallet(
    name="treasury-btc",
    chain="bitcoin",
    network="mainnet",  # mainnet | testnet (Bitcoin) | sepolia (Ethereum) | paseo (Polkadot)
    words=24,           # 12 or 24-word recovery phrase (defaults to 12)
    encryption_password=os.environ["GUVENO_ENCRYPTION_PASSWORD"],
)
print(created.first_address["address"])   # bc1...
print("Back this up:", created.mnemonic)  # shown once — store it safely

# Import an existing phrase as a new server-side wallet.
imported = guveno.import_wallet(
    name="restored-xrp",
    chain="xrp",
    network="mainnet",
    mnemonic="test test test test test test test test test test test junk",
    encryption_password=os.environ["GUVENO_ENCRYPTION_PASSWORD"],
)

Both return a Wallet that's already loaded and ready to use. A new phrase creates a new vault, named after the wallet unless you pass vault_name; team_id scopes the vault to a team (omit it to inherit your own team). Importing a phrase that already backs one of your vaults adds the wallet to that vault instead, provided you hold key access to it.

Vaults: one phrase, many chains

# Browse vaults (metadata only).
for vault in guveno.list_vaults():
    print(vault.name, vault.fingerprint[:8], vault.wallet_count, vault.access)

# Add another chain to an existing vault. The phrase is fetched sealed from the
# server and unlocked locally; nothing about the key is sent.
eth = guveno.add_wallet_to_vault(
    created.wallet.detail.vault_id,
    name="treasury-eth",
    chain="ethereum",
    network="mainnet",
    encryption_password=os.environ["GUVENO_ENCRYPTION_PASSWORD"],
)

# A second Ethereum wallet in the same vault needs its own derivation path.
ops = guveno.add_wallet_to_vault(
    created.wallet.detail.vault_id,
    name="ops-eth",
    chain="ethereum",
    network="mainnet",
    derivation_path_prefix="m/44'/60'/1'/0",
    encryption_password=os.environ["GUVENO_ENCRYPTION_PASSWORD"],
)

Every wallet record carries vault_id, vault_name, team_id, and derivation_path_prefix. list_wallets(vault_id=...) lists one vault's wallets. A custom prefix must match the chain's grammar (BIP32 with ' for hardened segments; //name// junctions on Polkadot), and the server rejects addresses that are not derived at exactly the wallet's prefix, so keep using derive_address() rather than registering addresses by hand.

Using a loaded wallet

wallet = guveno.load_wallet(wallet_id, encryption_password)

wallet.withdraw(asset_id=..., to_address=..., amount=...)                  # auto-select source
wallet.withdraw(address_id=..., asset_id=..., to_address=..., amount=...) # specific source
next_addr = wallet.derive_address(label="deposits")  # next address at the server index
page = wallet.list_addresses()
balances = wallet.get_balances()      # per-address, per-asset
totals = wallet.get_totals()          # aggregated per asset
stats = wallet.get_stats()            # holdings, flows, top addresses
phrase = wallet.reveal_mnemonic()     # audited server-side
status = wallet.get_withdrawal(withdrawal.id)

wallet.id; wallet.name; wallet.chain; wallet.network  # metadata properties
  • Per-source serialization — withdrawals from the same source (a specific address, or a wallet when auto-selecting) are queued so concurrent sends never collide on a nonce or reuse a UTXO; different sources run in parallel.
  • Idempotency — an idempotency_key is auto-generated per withdrawal; pass your own to make a retried withdraw(...) replay-safe. Without one, a retry prepares a new withdrawal.
  • All custodied chains are signable; Polkadot extrinsics are built fully offline from metadata the server includes in the prepared payload.

When a send's outcome is unknown

A broadcast is the one call here whose failure does not mean "nothing happened". If the node accepted the transaction and the reply was lost, the payment is out there. The SDK raises a distinct error for that case so you never mistake it for a refusal:

from guveno import SendOutcomeUnknownError

try:
    wallet.withdraw(address_id=..., asset_id=..., to_address=..., amount=...)
except SendOutcomeUnknownError as error:
    # The transaction MAY be live. Do not sign a replacement.
    alert_ops(error.withdrawal_id, error.tx_hash)

What to do with one:

  • Look up error.tx_hash on the chain. If it is there, the withdrawal settles by itself and wallet.get_withdrawal(id) will show it reach confirmed.
  • Or call guveno.client.broadcast_withdrawal(...) again with the identical signed_raw_tx. The same bytes are the same transaction, so resending them is not a second payment — and the server accepts only those bytes for this withdrawal.

What not to do: prepare again, sign again, or retry withdraw(). On an account chain a replacement is signed at the next nonce, so if the first transaction did land you have paid twice. The server enforces this too — until the send is resolved, preparing any new withdrawal from that address fails with a conflict, and the broadcast endpoint refuses any signed transaction other than the one it already holds.

Headless signing (KMS / HSM / file)

For automated signers that hold their own key material, load a wallet with a key provider instead of a password — no encryption password, and the sealed secret is never fetched from the server:

from guveno import Guveno, FileKeyProvider, KmsKeyProvider

guveno = Guveno(api_key=os.environ["GUVENO_API_KEY"])

# keys.json: { "<keyFingerprint>": "<bip39 mnemonic>", ... }
wallet = guveno.load_wallet(wallet_id, FileKeyProvider("/run/secrets/keys.json"))
wallet.withdraw(address_id=100, asset_id=1, to_address="0x70997...", amount="1.5")

Don't want a plaintext key file? Back it with AWS KMS, GCP KMS, Vault, or an HSM — the plaintext mnemonic exists only transiently in memory while a withdrawal is signed:

def unwrap(ciphertext: str, key_fingerprint: str) -> str:
    out = kms.decrypt(CiphertextBlob=base64.b64decode(ciphertext))
    return out["Plaintext"].decode("utf-8")  # the mnemonic

keys = KmsKeyProvider(entries={"<keyFingerprint>": "<base64 KMS ciphertext>"}, decrypt=unwrap)
wallet = guveno.load_wallet(wallet_id, keys)

Key-provider wallets can withdraw() and list_addresses(); derive_address() and reveal_mnemonic() need the encryption-password path (they seal/unseal against the server).

Balances

Reading balances needs only the API key — no encryption password or key provider. The quickest path is the getters on a loaded wallet, but guveno.client exposes the same endpoints if you only have a wallet id:

# Per-address, per-asset balances — one entry per address.
for addr in wallet.get_balances():
    for b in addr.balances:
        print(addr.address, b.asset.symbol, b.total)

# Aggregated per-asset totals across the wallet's addresses.
for t in wallet.get_totals():
    print(t.asset.symbol, t.total)

# Holdings, lifetime in/out flows, and the top-holding addresses.
stats = wallet.get_stats()
print(stats.address_count, stats.transaction_count)

total/amount are decimal strings in the asset's main unit (e.g. "1.5" ETH), and each carries the full asset (symbol, decimals, contract address, …) so you don't need a separate asset lookup.

Without a loaded wallet, call the client directly — and roll up the whole company with get_company_balance_summary:

totals = guveno.client.get_wallet_balance_totals(wallet_id)

# Company-wide totals per asset, optionally scoped to a chain/network.
company = guveno.client.get_company_balance_summary()
eth_only = guveno.client.get_company_balance_summary(chain="ethereum", network="mainnet")

Transactions and review marks

Transactions come off the client, newest first, with the usual filters and cursor pagination:

deposits = guveno.client.list_transactions(wallet_id=wallet_id, direction="incoming", limit=50)
page = guveno.client.list_transaction_page(chain="bitcoin", status="confirmed")
one = guveno.client.get_transaction(9001)

Each transaction carries a marks object — the review layer your team uses in the dashboard. There are two marks and they differ in who can see them:

  • savedpersonal. Your own "review later" queue. Another member's saved state is never reported to you, and yours is never reported to them.
  • flagged and noteshared, with the author attached. A flag says "this needs attention", so everyone who can already see the transaction sees who raised it and why.

saved / flagged / note at the top level are yours (an API key acts as its creator); shared lists every member's flag and note with its author:

tx = guveno.client.get_transaction(9001)
print(tx.marks.saved, tx.marks.flag_count)
for mark in tx.marks.shared:
    print(mark.user.email, "flagged" if mark.flagged else "noted", mark.note)

# Only the arguments you pass change — flagging leaves the ★ and the note alone.
guveno.client.set_transaction_marks(9001, flagged=True, note="unexpected fee")
# `note=None` means "leave it alone", so clearing is explicit:
guveno.client.set_transaction_marks(9001, clear_note=True)

# The two review lists, and their counts.
flagged = guveno.client.list_transactions(marked="flagged")
mine = guveno.client.list_transactions(marked="saved")
summary = guveno.client.get_transaction_mark_summary()
print(summary.saved, summary.flagged)

Marks are organizational only — they never affect crediting, confirmations or balances. Reading and writing them needs only the transactions:read action.

Webhooks and low-level access

The high-level facade covers wallet and vault operations. For everything else (webhooks, renaming or deleting wallets, listing withdrawals, vault administration) use the underlying client at guveno.client (a GuvenoApiClient):

webhook = guveno.client.create_webhook(
    type="api",
    config={"url": "https://example.com/webhooks/guveno"},
    events=["deposit.confirmed", "withdrawal.confirmed"],  # or ["*"] for all
)
print(webhook.signing_secret)  # shown only here — store it to verify x-guveno-signature

deliveries = guveno.client.list_webhook_deliveries(webhook.id, limit=50)

All event types are exported as WEBHOOK_EVENT_TYPES. Managing webhooks requires an API key with the webhooks:manage action.

Vault administration lives on the client too: create_vault() registers a phrase without a wallet yet (needs wallets:create), and update_vault() (rename or move to another team), delete_vault() (archive; refused while it has active wallets or funded addresses), list_vault_access(), grant_vault_access(), and revoke_vault_access() are accepted from dashboard user sessions only, because each one changes who can see or sign for every wallet in the vault. The server rejects API keys on those routes.

Supported chains

Chain Address style
Bitcoin native SegWit bc1...
Ethereum 0x...
BNB Smart Chain 0x... (same address as Ethereum)
XRP classic r...
Polkadot SS58 (1... mainnet)
TRON Base58Check T...

We're adding new chains regularly — these six are live today. All use standard HD derivation, so a recovery phrase restores the same accounts in any compatible wallet (Polkadot uses sr25519 substrate junctions, matching Polkadot-JS / Talisman; BSC shares Ethereum's derivation, matching MetaMask/Trust Wallet).

Optional key file

Enable Also require a key file during dashboard encryption setup or under Profile → Wallet encryption. Setup requires a fresh test unlock before saving. The file can be any format, including an image (non-empty, up to 20 MiB). Use its exact original bytes: copying or renaming is fine; edits, image compression, and metadata changes prevent unlocking. Keep a separate backup. Public/shared files provide little additional protection. A generated random file is also available.

Once enabled, both your encryption password and the file are required to unlock and recover wallet secrets through Guveno. Neither input is uploaded or stored by the SDK. Guveno cannot reset or recover them. An independently backed-up wallet recovery phrase can still recover that wallet. This setting protects your access across all wallets; other authorized members retain their own access. Old encrypted backups retain their old credentials, and keys already unlocked in another process remain usable until locked. Changing protection keeps wallet addresses and recovery phrases unchanged.

from pathlib import Path
key_file = Path('/secure/original-photo.png').read_bytes()
wallet = guveno.load_wallet(wallet_id, encryption_password, key_file=key_file)
# Use wallet, then wallet.lock() when finished.
del key_file  # Python cannot guarantee wiping immutable bytes from memory.

create_wallet() and import_wallet() also accept key_file: bytes alongside encryption_password. EncryptionSession.unlock() / unlock_from_user() and decrypt_user_private_key() accept key_file as their third argument. Omit it for password-only accounts; an unexpected file or combination with a key provider fails.

Release files for guveno 1.6.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for guveno 1.6.1
File Size Uploaded
guveno-1.6.1.tar.gz 58.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for guveno 1.6.1
File Interpreter ABI Platform
guveno-1.6.1-py3-none-any.whl Python 3 none any Details

Total release size: 132.2 kB

Release files / guveno-1.6.1.tar.gz

Download URL guveno-1.6.1.tar.gz
Size 58.0 kB
Tags Source
SHA-256 checksum
How to use checksums
09c644648ab6ffa1f1876eba862536430e154089849bc7f4b1e5b0716d55eeff
BLAKE2b-256 checksum
How to use checksums
6d5120990a5b207d425049f329d75a0154b0605515743dcdb751dc2e4e0ff327
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.2

Release files / guveno-1.6.1-py3-none-any.whl

Download URL guveno-1.6.1-py3-none-any.whl
Size 74.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cc0b6e837e2c265c98df169ac2bf9da147321a2ed0e62d9c7fb740d5fa89dd97
BLAKE2b-256 checksum
How to use checksums
216780695261f6a4511a2b84535872ac09ccbb16460e271fb34d4ba9a9e5b0d3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.2

Release history Release notifications | RSS feed

1.7.0

2 release files

1.6.3

2 release files

1.6.2

2 release files

This release

1.6.1 This release

2 release files

1.6.0

2 release files

1.4.0

2 release files

1.3.1

2 release files

1.3.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page