Wallet
A non-custodial Tempo balance — deposits, sends and conversions. Nothing on this surface can sign; the holder's own passkey is the only signer, on their own device.
Mixed status by resource
Wallet registration, wallet/balance/transaction and route reads, send source-hash recording, and conversion create/get/source-hash recording are available in the contract. Deposit quotes/intents, send-intent create/list/get, bank-service resources and conversion-pair listing are dark-flag. Check each operation in the reference and the account capabilities; contract status does not prove a live cohort or provider route is open. Wallet operations currently have no API test-mode support.
Concept
The wallet is the holder's own Tempo balance. The API can read it, price a deposit or a send, and build the unsigned steps for one. It holds no wallet key, so it cannot sign a send for you, and it cannot recover the wallet if no usable passkey or synced copy remains. Provider and token restrictions may still apply.
The USD total combines per-stablecoin balances; a send uses its route's source-token balance, not an automatic conversion of that total. Wallet-only crypto self-custody on Tempo requires no Swaps KYC. Receive-by-bank and bank withdrawals have separate holder-verification and provider-capability requirements.
Business keys are permitted on the wallet, balance, transaction, route and conversion-pair reads whose allowed-caller metadata explicitly includes business_key, where that operation is available (wallet.conversion_pairs.list is still dark-flag). They cannot use Wallet writes or every Wallet read.
Resources
| Object | What it is |
|---|---|
wallets / balances / transactions | The holder's address, per-stablecoin balances plus a computed USD total, and a bounded recent-activity window. |
virtual_accounts | Bank details the holder can receive a transfer into. What arrives is delivered to the wallet as stablecoin. See Receive by bank. |
deposit_intents | A one-time, amount-bound cross-chain deposit address — single-use, expires. |
send_intents | A prepared cross-chain withdrawal — the ordered steps the holder's passkey signs, quoted net of fees. |
offramp_intents / external_accounts | A prepared payout to the holder's own verified bank account — never a third party's, on anyone's behalf. |
conversions | An unsigned on-chain swap transaction, built but never sent by the API. |
Minimal flow (a cross-chain send)
GET /v1/wallet/balances— or the MCP toolget_wallet_balance.POST /v1/wallet/send_intents— prepare the withdrawal (prepare_wallet_withdrawal); nothing leaves the wallet yet. The recipient cannot be a Swaps Wallet address (422 destination_rail_keyless: a Swaps Wallet receives on Tempo only) or a deposit address Swaps issued (422 destination_swaps_deposit_address);503 destination_risk_unverifiablewhen that could not be checked.- The holder reviews the quoted amount and signs the returned steps with their own passkey.
POST /v1/wallet/send_intents/{id}/source_tx— record the broadcast hash (confirm_wallet_withdrawal) so settlement can be tracked.GET /v1/wallet/send_intents/{id}— orget_transfer_status— to watch it settle.
Receive by bank
A virtual account gives the holder bank details to receive a transfer into. GET /v1/wallet/virtual_accounts lists them and POST /v1/wallet/virtual_accounts creates one. Two fields say what kind of account a row is:
destination.railis the bank rail:ach,sepa,spei,faster_payments,pix,wireorbre_b. It has one spelling per rail: an account the provider sync recorded asach_pushis published asach, as an account created through/v1always was. It is an open enum, so a client tolerates a rail it does not know, and a stored rail outside the list is published as stored.provider_environmentissandbox,productionornull.sandboxmeans a provider test account: its bank details are test data and no real bank transfer arrives.nullmeans no environment was recorded and is never evidence that an account is live. Onlysandboxmarks a test account. Swaps does not work it out from the host or fromlivemode, which stays the caller's own mode.
Not every currency can be opened, and GET /v1/capabilities?product=wallet_bank says why in one order — the order POST /v1/wallet/virtual_accounts refuses by: owner_only, rail_not_enabled, verification_closed, payin_fiat_refused, endorsement_not_requestable, route_unavailable, collection_account_uses_currency, then a verification step with no blocked_reason, then payin_fiat_pending, and last wallet_not_found. First what nothing the person does opens, each not_enabled with action: none: owner_only when the caller is a member signed in as themselves who is not the account's owner (only the owner opens receive by bank); rail_not_enabled while receive by bank is not launched for the account; verification_closed when the provider has given its final answer on the account holder (a final rejection, or a paused or offboarded customer) or the holder has used every verification attempt — then the corridor carries detail_code: verification_attempts_exhausted, and support, not a retry, is the next step; payin_fiat_refused when the provider reports the holder's fiat pay-ins as inactive or rejected (the create reads the provider's own record again and refuses the same way); endorsement_not_requestable when a currency needs a provider endorsement the holder's provider customer does not carry, whatever the verification state (this corridor offers no step for it; a kind: kyc verification link with that rail asks the provider for it — see Customers); route_unavailable when the holder's Swaps Wallet is not on Tempo mainnet; and collection_account_uses_currency when a Payment links collection account already receives that currency into the same wallet. The create answers owner_only with a 403 before it reads anything: 403 role_denied for a member, at the operation's role gate, and 403 scope_denied for an admin who is not the owner. It answers rail_not_enabled with 409 capability_unavailable and details.reason: rail_not_enabled, route_unavailable with 409 network_not_supported, collection_account_uses_currency with 409 conflict, and the others with 409 capability_unavailable and the same details.blocked_reason. Do not send the person to verify for any of them. Then the person's own steps: a verification step with no blocked_reason; payin_fiat_pending (gathering_no_path) while an individual holder's fiat pay-in activation could not be read — the create answers 409 capability_unavailable with that details.blocked_reason; and last, when the account has no Swaps Wallet yet, wallet_not_found — the create answers 409 wallet_not_found: create the wallet first (POST /v1/wallet/wallets). When the holder is an individual whose provider fiat pay-in activation is still pending, every corridor of a launched account carries advisory: payin_fiat_pending, whatever its status (owner_only excepted): the provider may review a deposit into any of the holder's bank accounts and return it, including accounts created earlier and those of a previous wallet, which keep receiving while a corridor is closed — say so before the person shares the bank details of any account in that currency. If the provider itself refuses a create, the answer is 409 virtual_account_provider_refused (capability_unavailable), with details.provider_status and, when it is a known one, details.provider_code; no account is created, and the same request is refused again, so do not retry it. If the key of the holder's Swaps Wallet was reported lost to Swaps support, the create — and the reactivation of an account that pays into that wallet — answers 422 destination_wallet_key_reported_lost before the provider is asked: nothing is created or reopened, and the same request is refused again until support clears the record after a newer signature by the same key, so do not retry it. A wallet off Tempo, disabled, whose signer is at risk or whose key cannot be confirmed is refused the same way, each under its own code — 422 destination_rail_keyless, destination_wallet_disabled, destination_wallet_signer_at_risk, destination_wallet_signer_unconfirmed — and when the check itself could not run the answer is 503 destination_risk_unverifiable: nothing was created, retry after Retry-After with the same Idempotency-Key. The exact codes go only to the wallet's own holder; for anyone else's Swaps Wallet the answer is 422 destination_not_accepted, and every caller gets 503 destination_check_unavailable when the wallet or key-mark lookup itself failed. Only the account's owner can create one (403 role_denied for a member, 403 scope_denied for an admin who is not the owner). Read the capability before offering the account. See Providers and coverage.
Your own bank accounts
POST /v1/wallet/external_accounts saves a bank account the holder owns, and GET /v1/wallet/external_accounts lists them masked (last4, never the full number). attest_own_account: true is required on every create, and the account holder's name is never sent: it is the verified legal name. One rail per request:
rail | currency | Bank details |
|---|---|---|
ach, wire | usd | account_number, routing_number, account_type |
sepa | eur | iban, bic |
spei | mxn | clabe |
faster_payments | gbp | sort_code (six digits, no hyphens), account_number (eight digits) |
pix | brl | pix_key; optional pix_key_type: cpf, cnpj, email, phone, evp |
A Pix key is a CPF (11 digits) or a CNPJ (14 digits) — punctuation is removed and the check digits are verified —, a phone number as +55 and 11 digits, an email address, or a random key (a UUID). 11 bare digits are a CPF, never a phone number. Anything else is 400 invalid_request with param: pix_key. For a pix account, last4 is the unmasked tail of the provider's masked key preview, otherwise the last 4 digits of the CPF or CNPJ the key belongs to.
ach, wire, sepa and spei accounts receive the wallet's own bank withdrawal (offramp_intents). faster_payments and pix accounts are payout destinations for a sell order (Buy & sell); the withdrawal does not pay them. rail on the read is an open enum: tolerate a value you do not know. Bank details are REST-only and never an MCP tool argument.
Where a transfer came from
Each transfer in GET /v1/wallet/transactions (and GET /v1/wallet/transactions/{hash}, or the MCP tool list_wallet_activity) carries origin. On the list every row has it: null, or an object.
origin | Meaning |
|---|---|
{ "kind": "bank_deposit", "virtual_account_id": "va_…", "currency", "rail" } | An incoming transfer that one of the caller's own wallet virtual accounts recorded as the delivery of a bank deposit. currency and rail are that account's destination.currency and destination.rail, spelled the same. The deposit is still one transfer row. |
null | Any other transfer, and any transfer Swaps cannot tie to one account: a hash claimed by two accounts, an account you may not read, or an events lookup that failed. null is not proof that a transfer is not a bank deposit. |
Only an owner or admin who may read the account sees bank_deposit; a business key or another member reads null. It is never inferred from the sender, the amount or the timing, and it never carries a provider id or a bank account number. kind and rail are open enums; tolerate a value you do not know. On the single read, a missing origin means the same as null.
Degraded means unknown, never zero
A rate-limited chain read returns degraded: true with an empty list — never read that as "no activity" or a zero balance. History here is a bounded recent window, not a full ledger.
What an agent can and cannot do here
Every wallet-write tool prepares — prepare_wallet_withdrawal, create_deposit_address, prepare_bank_withdrawal — and returns unsigned steps or a deposit address; none of them broadcasts anything. Adding a bank destination is own-account only and re-verified against the provider at spend time — an agent can never add one on someone else's behalf.
A deposit address is opened only for a wallet whose key the server can confirm. While it cannot, POST /v1/wallet/deposit_intents and POST /v1/wallet/deposit_quotes answer 422 wallet_signer_at_risk before the provider is asked: nothing is created, and a retry does not change the answer until the key is confirmed — tell the holder to contact Swaps support, and do not offer another network or the wallet's own address. The same answer covers a wallet whose key its holder reported lost to Swaps support; its message then says so. When the check itself could not run they answer 503 wallet_signer_unreadable: nothing was created and the same request may be tried again. Reading an existing deposit intent is unaffected.
Next: the reference for the full intent schemas · Pay an invoice for the fiat-out counterpart that doesn't touch the wallet.