> Guveno API reference v1.10.0, last updated October 17, 2026. The whole reference is at https://www.guveno.com/llms-full.txt. ## Wallets A wallet is one chain and network inside a vault. It groups the on-chain addresses Guveno watches for you, all derived from the vault's recovery phrase at one derivation path. ### List wallets **GET** `/wallets` Query params: `limit`, `cursor`, `chain`, `network`, `name`, `vaultId`, `ids`. The `name` filter is an exact, case-sensitive match; since names are unique per chain and network, `name` + `chain` + `network` returns at most one wallet. This is the intended way to look a wallet up by name. `vaultId` returns the wallets of one vault. `ids` takes a comma-separated list and returns just those wallets — for when you already hold a set of ids and want their current rows in one request instead of paging the whole list. At most 100 per call; ask for more and the request is refused rather than answered in part. Scoping is unchanged, so an id your key cannot see simply does not come back: a short result means “not visible to this key”, never “deleted”. ``` curl "https://api.guveno.com/v1/wallets?chain=ethereum&limit=20" \ -H "Authorization: Bearer gv_live_your_key_here" # Look up one wallet by name curl "https://api.guveno.com/v1/wallets?name=treasury-eth&chain=ethereum&network=mainnet" \ -H "Authorization: Bearer gv_live_your_key_here" # Resolve a set of ids you already hold curl "https://api.guveno.com/v1/wallets?ids=12,7,3" \ -H "Authorization: Bearer gv_live_your_key_here" ``` ``` { "items": [ { "id": 12, "vaultId": "3e2b7c8a-0f65-4f7f-9a83-2e33cbe0d1a4", "vaultName": "Treasury", "keyId": "3e2b7c8a-0f65-4f7f-9a83-2e33cbe0d1a4", // same as vaultId (historical name) "teams": [{ "id": 4, "name": "Treasury" }], // who can see and use this wallet; [] = owners/admins only "name": "treasury-eth", "chain": "ethereum", "network": "mainnet", "derivationPathPrefix": "m/44'/60'/0'/0", "autoWithdrawal": true, // false = withdrawals must name the source address(es) "status": "active", "chainInPlan": true, // false = the chain left your plan; chain-using calls 403 "addressCount": 4, "balances": [ // aggregated holdings per asset (list view only) { "asset": { "symbol": "ETH", ... }, "total": "18.42" } ] } ], "pageInfo": { "nextCursor": null } } ``` Each wallet in the list carries `balances` (its holdings summed per asset across all addresses) so you can show a portfolio figure without a request per wallet. Combine it with `GET /prices` (below) for an estimated USD value. Testnet wallets are worth nothing, so price them at 0. `chainInPlan` reports whether the wallet's chain and network are still included in your organisation's plan. It turns `false` when a chain add-on is removed or the plan is downgraded: the wallet, its balances, and its history stay fully readable, but withdrawals, registering new addresses, and manual sync return `403` until the chain is back on the plan, at which point the block lifts immediately. Check it before preparing a withdrawal. ### Get a wallet **GET** `/wallets/:id` The detail adds `key` (the vault: `id`, `name`, `fingerprint`), `nextAccountIndex` (the next address index to derive), and a `keyAccess` grant when the caller holds access to the vault, but only its metadata (`id`, `keyId`, `userId`, timestamps). The encrypted secret is never returned here; fetch it from the dedicated endpoint below. ### Reveal the key secret **GET** `/wallets/:id/secret` The SDK and CLI call this for you when they derive an address or sign a withdrawal, and that is how it is meant to be used. It returns the caller's `keyAccess` grant including `encryptedSecretJson`: the wallet secret encrypted to your own public key, decryptable only on your device. Reach for it directly only if you are implementing signing yourself — unsealing and deriving by hand is easy to get wrong, and the mistakes cost funds rather than raising errors. An API key may read it, acting as the member who created the key, provided that member holds a copy of the vault's phrase (viewers can never hold one). Each successful read is recorded in the activity log as a secret-access event. Handing a copy to *someone else* is not done here, or anywhere in the API: see **Who holds the key**. ### Addresses **GET** `/wallets/:id/addresses` Query params: `limit`, `cursor`, `status` (`active`, the default; `archived`, or`all`) and `search` (matches the address or its label). **POST** `/wallets/:id/addresses/:addressId/unarchive` Restore a previously archived address. Requires an owner or admin role; an API key needs `wallets:update` on the wallet. ### Server-side address derivation **POST** `/wallets/:id/addresses` Derives and registers a receive address on the wallet and returns it, using only the API key — no encryption password, no key material at the caller. This is the endpoint to reach for when a platform needs a deposit address per customer on demand. It works because the wallet can register an **account xpub**: the extended **public** key at its `derivationPathPrefix`. Every chain except Polkadot, Solana and TON derives addresses on a BIP44 path whose tail is non-hardened, so that public key reproduces exactly this wallet's addresses — and cannot sign, authorize a withdrawal, or reveal the phrase it came from. Spending authority is unchanged: withdrawals are still prepared and signed on your side. We hold no key material, before or after. Register it with `accountXpub`, on `POST /wallets` at creation or `PATCH /wallets/:id` later, computed where the seed is — `accountXpubFromMnemonic()` in the Node SDK, `account_xpub_from_mnemonic()` in the Python SDK, or `guveno enable-derivation ` in the CLI. Before storing it we re-derive the addresses already on the wallet and require every one to match; a single mismatch rejects the request and nothing is stored. An extended *private* key is rejected outright rather than neutered for you. Send `null` to turn it back off. In the dashboard it is a switch: **Wallet → Settings → Address generation**. Turning it on asks for your encryption password, computes the account xpub in your browser and registers it — the same request, with the phrase never leaving your device. The card names the wallet's current state, and turning it off drops the stored key. Send no body and the server owns the index: two concurrent calls return two consecutive addresses rather than racing for one, so the same address can never be handed to two customers. Wallets report `serverDerivation` so you can tell which are enabled; the xpub itself is never returned. Send `accountIndex` and you own it instead. The use for it is giving one customer the same address on two wallets that share a vault and a derivation prefix, and so an account xpub — the same index derives the same address on each, which is how an EVM deposit address can be identical across chains. An index already registered on that wallet is refused with `409`, archived addresses included: nothing is reassigned and no free index is substituted, because the address sitting on a taken index may already belong to somebody else. Allocation stays highest + 1, so pinning a high index moves that wallet's next automatic index above it. Valid indexes run from `0` to `2147483647`, BIP32's non-hardened ceiling. ``` curl -X POST https://api.guveno.com/v1/wallets/42/addresses \ -H "Authorization: Bearer gv_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{"label": "customer-4471", "accountIndex": 41}' ``` `409` when the wallet has not opted in, when the requested index is taken, and always for Polkadot, Solana and TON, whose paths are hardened at every level (Polkadot's `//i` junctions, the `'` segments on Solana and TON), which no public key can follow. Those wallets keep deriving client-side. Generating requires the `wallets:generate_address` action; registering the xpub requires `wallets:update`, deliberately not the same one, so a key that may use server derivation cannot also enable it. ### Creating wallets **POST** `/wallets` **PATCH** `/wallets/:id` **DELETE** `/wallets/:id` Creating a wallet involves deriving addresses and encrypting key material on your side. The simplest path is the Node.js or Python SDK, which builds the request payload for you. Every wallet belongs to a vault, and the body names the vault in one of two ways: - **An existing vault**: send `vaultId`. You must already hold key access to that vault (otherwise `403`), and no seed material travels with the request. Use this to add another chain, or another derivation path on the same chain, to a vault you hold. - **A recovery phrase**: send `keyFingerprint` and `encryptedSecretJson`. If a vault already owns that fingerprint (and you hold access to it), the wallet joins it; otherwise a new vault is created, named `vaultName` if you give one and after the wallet if not. ``` # Add an Ethereum wallet to a vault you already hold curl -X POST https://api.guveno.com/v1/wallets -H "Authorization: Bearer gv_live_your_key_here" -H "Content-Type: application/json" -d '{ "name": "treasury-eth", "chain": "ethereum", "network": "mainnet", "vaultId": "3e2b7c8a-0f65-4f7f-9a83-2e33cbe0d1a4", "addresses": [ { "address": "0xabc...", "derivationPath": "m/44'''/60'''/0'''/0/0", "accountIndex": 0, "label": "main" } ] }' # Register a wallet from a recovery phrase (creates the vault if the fingerprint is new) curl -X POST https://api.guveno.com/v1/wallets -H "Authorization: Bearer gv_live_your_key_here" -H "Content-Type: application/json" -d '{ "name": "treasury-btc", "chain": "bitcoin", "network": "mainnet", "vaultName": "Treasury", "keyFingerprint": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "encryptedSecretJson": { "version": 2, "type": "x25519-sealed-box", "ciphertext": "..." }, "addresses": [ { "address": "bc1q...", "derivationPath": "m/84'''/0'''/0'''/0/0", "accountIndex": 0 } ] }' ``` Sending `vaultId` together with `keyFingerprint`, `encryptedSecretJson` or `vaultName` is a `400`: the existing vault already defines them. `teamIds` decides who can see the wallet and is accepted either way; omit it on a plan without teams and the wallet joins your company's default team, which holds every member. Change it later with `PATCH /wallets/:id` (owners and admins, dashboard sessions only). Wallet names are unique per chain and network within your company. Deleting a wallet archives it; the vault stays until you archive it too. Address updates via `PATCH` are additive: the `addresses` you send are created or updated, and existing addresses are left untouched (never archived by omission). An address that holds a balance can't be archived, and deleting a wallet is rejected while any of its addresses still hold funds. Registering an address this way says you derived it from the vault's seed, and the server holds you to that. The caller must hold key access to the wallet's vault, whatever else the body carries (an API key is held to its owner's access); a role alone is not enough and gets a `403`. Every address has to be a well-formed address for the wallet's chain and network — checksum, encoding and network prefix are checked, and a typo or an address pasted from another chain is a `400`. And on a wallet with server-side derivation enabled, each address must be the one the registered account xpub derives at that index; anything else is refused, and one bad address rejects the whole batch. The SDKs and the dashboard derive addresses that pass all three by construction. `autoWithdrawal` (boolean, default `true`) controls how withdrawals choose a source. When `true`, a withdrawal that supplies only `walletId` lets the server auto-pick which address(es) (and, for Bitcoin and Dogecoin, which UTXOs) to draw from. Set it to `false` (on `POST /wallets` or later via `PATCH /wallets/:id`) to keep a wallet's funds segregated: withdrawals must then name the source explicitly with `addressId` (or `sourceAddressIds` on Bitcoin and Dogecoin), and an auto `walletId`-only prepare is rejected. ### Derivation paths Each wallet carries `derivationPathPrefix`, the account-level path its addresses hang off. Every address you register must sit at exactly `/` (Polkadot: ``; Solana and TON end in hardened segments, see the table); anything else is a `400`. Omit the prefix on create to get the chain standard: | Chain | Default prefix | | --- | --- | | Ethereum, BNB Smart Chain, Polygon, Arbitrum | `m/44'/60'/0'/0` | | Bitcoin mainnet | `m/84'/0'/0'/0` | | Bitcoin testnet | `m/84'/1'/0'/0` | | Dogecoin mainnet | `m/44'/3'/0'/0 (legacy P2PKH)` | | Dogecoin testnet | `m/44'/1'/0'/0` | | XRP | `m/44'/144'/0'/0` | | TRON | `m/44'/195'/0'/0` | | Solana | `m/44'/501' (hardened only; addresses at m/44'/501'/'/0')` | | TON | `m/44'/607' (hardened only; addresses at m/44'/607'/')` | | Polkadot | `// (hard junctions, e.g. //0)` | A vault holds at most one wallet per chain, network and prefix; a second wallet at the same prefix would derive the same addresses, so it is refused with `409`. To hold two wallets on one chain in the same vault, give the second one a different prefix (for example `m/44'/60'/1'/0`, the next BIP44 account). Prefixes are validated against a strict grammar: BIP32 paths use `m/` and `'` for hardened segments only; Polkadot prefixes are `//` or a chain of hard junctions ending in `//`, such as `//treasury//`; Solana prefixes are hardened-only BIP32 paths such as `m/44'/501'`, with each address at `/'/0'`; TON prefixes are hardened-only too, such as `m/44'/607'`, with each address at `/'`. Solana, TON and Polkadot addresses can only be derived from the recovery phrase, so those wallets are never server-derived: the SDK or the dashboard registers each address. A non-standard prefix will not be found by ordinary wallet software during recovery, so record it with your backups. Finally, one on-chain address belongs to exactly one wallet in your company; registering it under a second wallet is a `409`. **Restoring TON outside Guveno.** A Guveno recovery phrase is a standard BIP39 phrase. TON addresses are derived from it with SLIP-0010 ed25519 at `m/44'/607'/'` and use the v4r2 wallet contract, the layout OneKey and Tether's WDK use for BIP39 TON wallets. Tonkeeper and most TON-native wallets use their own phrase format instead of BIP39, so they will not import a Guveno phrase or will derive different addresses from it. Restore with a wallet that accepts a BIP39 phrase on that path. Source: https://www.guveno.com/docs/wallets/