Skip to content

HTTP / JSON API

walletrs exposes the same RPC handler set as the gRPC surface over HTTP/JSON. Routes are generated at build time from (google.api.http) annotations on proto/walletrpc.proto — adding a new RPC is a one-line proto annotation, no Rust glue. Use this surface from browsers, curl, scripts, polyglot clients, or anything that prefers HTTP over protoc tooling.

The HTTP gateway listens on WALLETRS_HOST:WALLETRS_HTTP_PORT (default 127.0.0.1:8080). Bearer-token auth is shared with the gRPC surface — see Authentication.

Every RPC is reachable as:

  • Method: POST
  • Path: /wallet/<snake_case_method> (see the mapping table below)
  • Headers:
    • Authorization: Bearer <token> (except for /wallet/ping)
    • Content-Type: application/json
  • Body: JSON encoding of the request message
  • Response: JSON encoding of the response message, or an error envelope (see error mapping)

Example:

Terminal window
TOKEN=...
# Liveness — no auth required
curl -sS -X POST http://127.0.0.1:8080/wallet/ping \
-H 'content-type: application/json' \
-d '{}'
# Authenticated call
curl -sS -X POST http://127.0.0.1:8080/wallet/create_system_managed_key \
-H "authorization: Bearer $TOKEN" \
-H 'content-type: application/json' \
-d '{"user_id":"alice","device_id":"alice-device-1","key_name":"primary"}'

The gateway uses pbjson for prost ↔ JSON conversion, which implements proto3 JSON semantics:

  • Numeric fields stay numeric (no JSON-string coercion). uint64 etc. are emitted as numbers — note that JS clients larger than 2^53 should treat large uint64 as string-typed in their schema.
  • bytes fields are base64-encoded strings.
  • Enums use the canonical name as a string (e.g. "SCRIPT_TYPE_TAPROOT"), not the integer value.
  • Field names use camelCase or the proto’s snake_case — both are accepted on input. Output is camelCase by default.
  • Empty messages serialize as {}.

For request messages with no fields (e.g. PingRequest), pass an empty JSON object: {}.

HTTP pathgRPC methodPurpose
POST /wallet/pingPingLiveness probe; bypasses auth.
POST /wallet/create_generic_walletCreateGenericWalletCompile spending conditions into a Bitcoin wallet (single-sig, multisig, or taproot with recovery leaves).
POST /wallet/create_system_managed_keyCreateSystemManagedKeyGenerate a new key inside walletrs; private material stored envelope-encrypted under WALLETRS_KEK.
POST /wallet/create_customer_managed_keyCreateCustomerManagedKeyRegister a customer-supplied xpub (private key never leaves the client).
POST /wallet/get_managed_keyGetManagedKeyFetch a managed key by (user_id, device_id, key_type).
POST /wallet/list_managed_keysListManagedKeysList managed keys, optionally filtered by user or type.
POST /wallet/getGetWalletRead wallet metadata + balance.
POST /wallet/updateUpdateWalletSync chain state from electrs.
POST /wallet/reveal_next_addressRevealNextAddressReveal num unused addresses (external by default; pass change: true for change addrs).
POST /wallet/list_addressesListAddressesList all revealed addresses.
POST /wallet/get_transactionsGetWalletTransactionsWallet’s transaction history.
POST /wallet/get_utxosGetWalletUtxosWallet’s spendable UTXOs.
POST /wallet/fund_transactionFundWalletTransactionBuild an unsigned PSBT to a destination. For taproot, set selected_leaf_hash to pick a spending path.
POST /wallet/sign_transactionSignWalletTransactionIn-process sign with a system-managed key.
POST /wallet/add_verify_transaction_signatureAddVerifyTransactionSignatureCombine an externally-signed PSBT into the in-flight one (hardware-wallet flow).
POST /wallet/finalize_transactionFinalizeWalletTransactionFinalize a fully-signed PSBT.
POST /wallet/broadcast_transactionBroadcastWalletTransactionBroadcast the finalized tx through electrs.
POST /wallet/get_spending_pathsGetWalletSpendingPathsList available taproot spending paths for a wallet.

The mapping is generated from (google.api.http) annotations in proto/walletrpc.proto, so this table mirrors the proto exactly.

Handler errors are emitted as tonic::Status and mapped to standard HTTP status codes. The body is always { "code": <int>, "message": <string> }, where code is the gRPC status code as an integer.

gRPC codeHTTP statusBody field code
OK200 OK0
INVALID_ARGUMENT, OUT_OF_RANGE, FAILED_PRECONDITION400 Bad Request3, 11, 9
UNAUTHENTICATED401 Unauthorized16
PERMISSION_DENIED403 Forbidden7
NOT_FOUND404 Not Found5
ALREADY_EXISTS, ABORTED409 Conflict6, 10
RESOURCE_EXHAUSTED429 Too Many Requests8
CANCELLED4991
INTERNAL, UNKNOWN, DATA_LOSS500 Internal Server Error13, 2, 15
UNIMPLEMENTED501 Not Implemented12
UNAVAILABLE503 Service Unavailable14
DEADLINE_EXCEEDED504 Gateway Timeout4

Example:

Terminal window
$ curl -sS -i -X POST http://127.0.0.1:8080/wallet/get \
-H "authorization: Bearer $TOKEN" \
-H 'content-type: application/json' \
-d '{"wallet_id":"never-existed"}'
HTTP/1.1 404 Not Found
content-type: application/json
{ "code": 5, "message": "Wallet not found" }

A minimal end-to-end script (regtest assumed):

Terminal window
TOKEN=$(grep 'STORE THIS' walletrs.log | tail -1 | awk '{print $NF}')
curl -sS -X POST http://127.0.0.1:8080/wallet/ping \
-H 'content-type: application/json' -d '{}'
curl -sS -X POST http://127.0.0.1:8080/wallet/create_system_managed_key \
-H "authorization: Bearer $TOKEN" \
-H 'content-type: application/json' \
-d '{"user_id":"alice","device_id":"alice-hot","key_name":"primary"}'
curl -sS -X POST http://127.0.0.1:8080/wallet/create_generic_wallet \
-H "authorization: Bearer $TOKEN" \
-H 'content-type: application/json' \
-d '{
"user_id": "alice",
"wallet_id": "alice-wallet-1",
"network": "regtest",
"preferred_script_type": "SCRIPT_TYPE_AUTO",
"spending_conditions": [{
"id": "primary",
"is_primary": true,
"timelock": 0,
"threshold": 1,
"policy": "SINGLE",
"managed_key_ids": ["alice-hot"]
}]
}'

For a fuller worked flow, see the end-to-end guide.

The HTTP routes are generated from the proto, so HTTP and gRPC always agree on the request / response shape. Adding a new RPC requires only the (google.api.http) annotation on the proto plus the usual handler addition; the HTTP route appears automatically on the next build.