> Guveno API reference v1.10.0, last updated October 17, 2026. The whole reference is at https://www.guveno.com/llms-full.txt. ## Withdrawals Withdrawals are a three-step flow: prepare an unsigned transaction, sign it locally with your key material, then broadcast it. > Send an `Idempotency-Key` on both calls. If a broadcast fails in a way that leaves the outcome unknown, resend the **identical** `signedRawTx` — never a newly signed one. Signing never happens on Guveno: the `signingPayload` you get from `prepare` is signed in your environment (usually by the SDK or CLI), then broadcast. ### Prepare **POST** `/withdrawals/prepare` Returns the withdrawal record plus a `signingPayload` for you to sign offline / client-side. Choose the source with **either** `walletId` **or** `addressId` (exactly one): - `walletId`: **auto-select**. The server picks a source with enough available balance. For account chains (Ethereum, BNB Smart Chain, Polygon, Arbitrum, Solana, TON, XRP, Polkadot, TRON) it picks one address whose balance covers the amount; for Bitcoin and Dogecoin it **aggregates UTXOs across the wallet** and returns change to a wallet-owned address. - `addressId`: **advanced**. Send from one specific address. (Bitcoin and Dogecoin can instead pass `sourceAddressIds`, an array, to restrict aggregation to a chosen subset of the wallet's addresses.) Because account-based chains can't combine balances in a single transaction, an auto withdrawal fails with `422` if no single address covers the amount. The asset must be on your organization's asset list (see **Assets**). Anything else is refused with `403`. Auto-select also requires the wallet's `autoWithdrawal` setting to be on (the default). If a wallet has `autoWithdrawal: false` (see `POST`/`PATCH /wallets`), a `walletId`-only prepare is rejected with `400`: you must name the source explicitly with `addressId` (or `sourceAddressIds` on Bitcoin and Dogecoin) so funds stay segregated per address. ``` # Auto-select (recommended): let the server pick the source curl https://api.guveno.com/v1/withdrawals/prepare \ -H "Authorization: Bearer gv_live_your_key_here" \ -H "Idempotency-Key: 8f1c-prepare-001" \ -H "Content-Type: application/json" \ -d '{ "walletId": 12, "assetId": 3, "amount": "0.25", "toAddress": "0xRecipient..." }' # Advanced: send from a specific address curl https://api.guveno.com/v1/withdrawals/prepare \ -H "Authorization: Bearer gv_live_your_key_here" \ -H "Idempotency-Key: 8f1c-prepare-002" \ -H "Content-Type: application/json" \ -d '{ "addressId": 50, "assetId": 3, "amount": "0.25", "toAddress": "0xRecipient..." }' ``` **XRP destination tags**: pass `destinationTag`, an integer from `0` to `4294967295`, when the recipient needs one (exchanges usually do). It is carried into the signed Payment, and a signed transaction whose tag does not match is refused at broadcast. It is accepted only on XRP: sending one on any other chain is a `400`, since nothing there could carry it. ``` curl https://api.guveno.com/v1/withdrawals/prepare \ -H "Authorization: Bearer gv_live_your_key_here" \ -H "Idempotency-Key: 8f1c-prepare-003" \ -H "Content-Type: application/json" \ -d '{ "walletId": 14, "assetId": 5, "amount": "25", "toAddress": "rRecipient...", "destinationTag": 1433698153 }' ``` **TON memos**: pass `memo`, the text comment the recipient reads, when they need one (exchanges usually do, and match it exactly). It must be 1 to 120 bytes of UTF-8 with no control characters, so no newlines or tabs; a longer memo is refused rather than split. It goes into the signed message (for a jetton, inside the token transfer, where the recipient's notification carries it), and a signed message whose memo does not match byte for byte is refused at broadcast. It is accepted only on TON: sending one on any other chain is a `400`, since nothing there could carry it. ``` curl https://api.guveno.com/v1/withdrawals/prepare \ -H "Authorization: Bearer gv_live_your_key_here" \ -H "Idempotency-Key: 8f1c-prepare-004" \ -H "Content-Type: application/json" \ -d '{ "walletId": 21, "assetId": 9, "amount": "40", "toAddress": "UQRecipient...", "memo": "4471203" }' ``` **TON addresses**: the addresses Guveno gives you are the non-bounceable form, `UQ…` on mainnet and `0Q…` on testnet, because a wallet that has never sent anything is not deployed yet and has to be paid non-bounceable. A destination must be the user-friendly form (`EQ…`/`UQ…`, or `kQ…`/`0Q…` on testnet) for the network you are sending on. We keep the bounce flag of the address you paste: `EQ…` asks for the payment to come back if the recipient cannot accept it, `UQ…` delivers it regardless. The raw `0:…` form is refused because it carries no bounce flag, and so is a jetton wallet pasted in place of its owner: send to the recipient's own wallet address and their jetton wallet is worked out from it. **Jetton sends** are paid for in GRAM, the TON network's coin (formerly Toncoin). Besides the network fee, each one attaches 0.05 GRAM to the jetton transfer to run the jetton wallets on both sides, and most of that comes back to the source address once the transfer settles. Prepare refuses a jetton send when the source address does not hold enough GRAM for both, so keep some GRAM next to your jettons. ### Broadcast **POST** `/withdrawals` Submit the signed transaction to broadcast it on-chain. ``` curl https://api.guveno.com/v1/withdrawals \ -H "Authorization: Bearer gv_live_your_key_here" \ -H "Idempotency-Key: 8f1c-broadcast-001" \ -H "Content-Type: application/json" \ -d '{ "withdrawalId": 7, "signedRawTx": "0x02f8..." }' ``` Guveno records your signed transaction, and the hash it will have, **before** handing it to a node, and submits it exactly once. That record is what makes the failure cases below safe. ### When a send’s outcome is unknown A broadcast is the one call in the API whose failure does not mean “nothing happened”. If a node accepted the transaction and the reply was lost, the payment is on the network even though your request failed. Guveno reports that case as `502`, with the committed `txHash` in the body: ``` { "statusCode": 502, "message": "The network did not confirm whether this transaction was accepted. ...", "txHash": "0x9a1f..." } ``` Two safe ways to resolve it, and one dangerous one: - **Look up the hash on-chain.** If the transaction is there, the withdrawal settles by itself and `GET /withdrawals/:id` will move to `broadcast` then `confirmed`. - **Resend the identical bytes.** Call `POST /withdrawals` again with exactly the same `signedRawTx`. The same bytes are the same transaction, so this is not a second payment. - **Never sign a replacement.** On an account chain a new signature means a new nonce, so if the first transaction did land, the recipient is paid twice. The API enforces the last point rather than trusting callers. While a send is unresolved: `POST /withdrawals` returns `409` for any `signedRawTx` other than the one it already holds; replaying `POST /withdrawals/prepare` with that withdrawal's idempotency key returns `409` instead of issuing a fresh signing payload; and preparing **any new withdrawal from the same address** returns `409` until the outstanding one is reconciled. Contact support if a transaction never arrives. A `400` from a broadcast is different: it means the node refused the transaction on its merits (most often the source can't fund gas). Nothing was sent, the withdrawal stays `prepared`, and you can fix the problem and broadcast again. ### Idempotency Both calls require an `Idempotency-Key` header. Reusing the same key returns the original result instead of creating a duplicate. For `prepare` that makes a retry safe on any error. For `broadcast` the key alone is not enough — reuse the key **and** the same `signedRawTx`, as above. Both endpoints need `withdrawals:create` granted on the source wallet, and the member behind the key needs a company role that can withdraw (`signer` or above). ### Signing & API keys An API key can `prepare` and `broadcast` withdrawals, but it **cannot sign them**. Signing uses the wallet's private key, which lives only with you. Guveno never holds spendable key material: the most a key with a signing action can read is the wallet's secret sealed to your own key (`GET /wallets/:id/secret`), which only your encryption password opens. So the middle step (turning the `signingPayload` into `signedRawTx`) runs in your own environment, typically via the SDK or CLI which hold the mnemonic. A fully unattended signer therefore needs your key material on the machine that calls the API, so plan your custody accordingly. ### List **GET** `/withdrawals` **GET** `/withdrawals/:id` Returns newest-first. Optional filters combine (AND): `walletId`, `addressId`, `chain`, `network` and `status` (`prepared`, `broadcast`, `confirmed` or `rejected`). Set the page size with `limit` (default 100, max 200) and pass the returned `pageInfo.nextCursor` back as `cursor` to fetch the next page. A null cursor means you've reached the end. `sourceAddresses` names every address the withdrawal spends from, each once, with the address `addressId` points to first. Account chains have exactly one; a Bitcoin or Dogecoin withdrawal also lists every address whose coins it spends. The set is fixed at prepare. ``` curl "https://api.guveno.com/v1/withdrawals?walletId=12&status=broadcast&limit=50" \ -H "Authorization: Bearer gv_live_your_key_here" ``` ``` { "items": [ { "id": 7, "addressId": 50, "sourceAddresses": ["0x9999..."], "assetId": 3, "network": "mainnet", "toAddress": "0xRecipient...", "amount": "0.25", "status": "broadcast", "txHash": "0x02f8...", "createdAt": "2026-06-14T11:55:00.000Z" } ], "pageInfo": { "nextCursor": "eyJpZCI6N30", "hasNextPage": true } } ``` ### Withdrawal requests **POST** `/withdrawal-requests` **GET** `/withdrawal-requests` **GET** `/withdrawal-requests/:id` The prepare → sign → broadcast flow assumes the caller can sign. A withdrawal **request** is for the case where it cannot, and shouldn't: an integration proposes a payout, and a person reviews and signs it. Creating a request moves no funds, signs nothing, and authorizes nothing — it records an intent. A leaked API key gets you a proposal somebody still has to approve. Requests are part of the **Pro and Enterprise** plans: on other plans `POST /withdrawal-requests` answers `403`, and so does approving one in the dashboard. Reading and declining are never gated, so requests already waiting stay visible after a downgrade. The key needs `withdrawals:request` on the wallet — not `withdrawals:create`, which is the signing action and is what the approver spends. A key holding only `withdrawals:create` cannot propose, and one holding only `withdrawals:request` can never send; that split is the point. Two separations hold, and both are enforced server-side. **An API key can never decide**: approving and declining require an interactive session. **An approver must be able to sign** — they need key access on the wallet, checked before anything is reserved. On top of that, a request a *person* proposed needs a different person to approve it; one an API key proposed may be approved by the key's own member, since that member could withdraw directly anyway and requiring a second human would lock a one-person company out of the flow entirely. ``` curl -X POST "https://api.guveno.com/v1/withdrawal-requests" \ -H "Authorization: Bearer gv_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "walletId": 12, "assetId": 3, "toAddress": "0xRecipient...", "amount": "1.5", "idempotencyKey": "exchange-withdrawal-991", "reference": "exchange tx 991", "approverUserId": 5 }' ``` Idempotent on `idempotencyKey`: a repeat with the same key returns the original request, so a caller that already debited a user and then lost the response can retry without proposing a second payout. The same key with different parameters is a `409`, never a silent overwrite. `approverUserId` asks a specific member to review it — they are emailed and notified, and the request shows as waiting on them. It is advisory: any other authorized member can still approve, so an approver who is away cannot strand a payout. `expiresInSeconds` runs from 5 minutes to 30 days (7 days by default); an unanswered request expires rather than lingering as a standing authorization to move funds. On XRP, `destinationTag` (an integer from `0` to `4294967295`) is kept on the request exactly as proposed, and it is what the approver signs. It is refused on any other chain. On TON, `memo` (1 to 120 bytes of UTF-8, no control characters) is kept on the request the same way and signed exactly as proposed. It is refused on any other chain. **Approving and declining happen in the dashboard.** The approver opens the request, reviews it and signs it in their browser. The destination, amount and asset come from the stored request, so the approver signs exactly what was proposed, and the request becomes `approved` once that broadcast succeeds. There is deliberately no route an API key alone can reach that moves funds, or that decides a request. Poll `GET /withdrawal-requests/:id` for the outcome. `approved` carries `withdrawalId` — read that withdrawal for the hash and confirmations. `rejected` means a person declined it (`declineReason` says why) and `expired` means nobody acted in time; neither produces a withdrawal and neither is retried, so propose a new request if you still want the payout. Note that a decline emits no webhook — `withdrawal.broadcast` fires for an approval because a withdrawal exists, but a declined request never becomes one, so polling is how you learn about it. ### Spending policies An organization on **Pro or Enterprise** can set spending policies in the dashboard (Security, then Policies): a limit per transaction, a rolling limit per hour, day, week or month, a cap on how many withdrawals happen in a window, a list of approved destinations, a check on the first payment to a new address, or business hours. Each one applies to the whole organization or to chosen wallets, teams, people, roles or API keys, and either blocks the withdrawal, sends it for approval, or lets it through and tells the owners and admins. Policies are created and changed only in the dashboard, and relaxing one waits for a second owner or admin. An API key cannot read or change them. They do apply to everything an API key does. `POST /withdrawals/prepare` and `POST /withdrawal-requests` are checked before anything is reserved, and a refusal is a `403` with a `code` (see *Errors*). When the answer is `policy_approval_required`, propose the same payout as a withdrawal request instead. A request a policy holds says what it is waiting for: ``` { "request": { "id": 7, "status": "pending_approval", "policyRequirements": [ { "policyId": 2, "policyName": "Large withdrawals", "approvals": 2, "approverRoles": ["owner", "admin"] } ], "approvals": [{ "userId": 5, "createdAt": "2026-10-05T14:02:11.000Z" }], ... } } ``` `policyRequirements` is empty when only the usual single approver is needed. `approvals` lists the people who have signed off so far; the member who finally signs counts as the last approval, and whoever proposed it (including through their own API key) never counts. USD limits use the current price, and when no price is available the withdrawal is treated as over the limit, never under it. Source: https://www.guveno.com/docs/withdrawals/