Self-Custody Wallet
Get network parameters
https://api.gobtcpay.com/public/api/v1.2/selfcustody/configReturns the parameters a client needs to work with BTC locally: network (mainnet / testnet), nativeSymbol and nativeDecimals, the default derivation path, the dust threshold in satoshis, and explorer URL templates for addresses and transactions (both contain {value} for the client to substitute). These describe the INSTALLATION, one deployment serves exactly one network, so there is no chain or token registry to query.
Authorization: walletAuth
Responses
idstringrequired3fa85f64-5717-4562-b3fc-2c963f66afa6resultobjectrequiredcurl -X POST "$BASE_URL/selfcustody/config" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"result": {
"$case": "success",
"success": {
"network": "mainnet",
"nativeSymbol": "string",
"nativeDecimals": 0,
"defaultDerivationPath": "string",
"dustThresholdSats": "string",
"explorerAddressUrlTemplate": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
"explorerTxUrlTemplate": "string"
}
}
}Register wallet
https://api.gobtcpay.com/public/api/v1.2/selfcustody/wallet/registerCreates a self-custody wallet for the public key in the JWT. The private key never reaches the server. One public key maps to at most one wallet: calling this again returns the existing walletId with created: false and does NOT overwrite name. A freshly registered wallet has no addresses, add them with /selfcustody/address/add.
Authorization: walletAuth
Body required
application/jsonnamestringrequiredSatoshi CoffeeResponses
idstringrequired3fa85f64-5717-4562-b3fc-2c963f66afa6resultobjectrequiredcurl -X POST "$BASE_URL/selfcustody/wallet/register" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"name": "Satoshi Coffee"
}'{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"result": {
"$case": "success",
"success": {
"walletId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"created": true
}
}
}Get wallet
https://api.gobtcpay.com/public/api/v1.2/selfcustody/wallet/getReturns the wallet of the authenticated public key with its addresses, each carrying the derivation path and index the client used. An empty addresses list is a valid state, not an error. monitoredAt: null signals an address that never reached the indexer, balances and history for it would be empty, so it is surfaced rather than hidden.
Authorization: walletAuth
Responses
idstringrequired3fa85f64-5717-4562-b3fc-2c963f66afa6resultobjectrequiredcurl -X POST "$BASE_URL/selfcustody/wallet/get" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"result": {
"$case": "success",
"success": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "Satoshi Coffee",
"createdAt": "string",
"addresses": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"address": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
"pubkeyHex": "02a1633cafcc01ebfb6d78e39f687a1f0995c62fc95f51ead10a02ee0be551b5dc",
"derivationPath": "string",
"addressIndex": 0,
"monitoredAt": "string"
}
]
}
}
}Get balances
https://api.gobtcpay.com/public/api/v1.2/selfcustody/wallet/get-balancesReturns per-address balances split by availability, all in satoshis: spendableSats (confirmed and unlocked, the only funds that can be spent now), unconfirmedSats (outputs of transactions not yet in a block, including our own change), lockedSats (outputs reserved by an unfinished /selfcustody/transaction/build) and totalSats, which is the sum of the three. The buckets never overlap, so they can be displayed side by side. spendableInUsd is null when a rate is unavailable, that is not a request failure, the satoshi amounts are still exact. Pass addressId to narrow to one address.
Authorization: walletAuth
Body required
application/jsonaddressIdstringbc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdqResponses
idstringrequired3fa85f64-5717-4562-b3fc-2c963f66afa6resultobjectrequiredcurl -X POST "$BASE_URL/selfcustody/wallet/get-balances" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"addressId": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"
}'{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"result": {
"$case": "success",
"success": {
"balances": [
{
"addressId": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
"address": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
"spendableSats": "string",
"unconfirmedSats": "string",
"lockedSats": "string",
"totalSats": "string",
"spendableInUsd": 0
}
],
"updatedAt": "string"
}
}
}Resync with the blockchain
https://api.gobtcpay.com/public/api/v1.2/selfcustody/wallet/resyncForces an out-of-schedule resync of the wallet's addresses. Pass addressId to resync one, or deep: true to re-read the whole history instead of only new blocks. Results are reported per address: a failure on one address comes back as ok: false with an error rather than failing the whole request, so a partial refresh is never mistaken for a complete one.
Authorization: walletAuth
Body required
application/jsonaddressIdstringbc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdqdeepbooleantrueResponses
idstringrequired3fa85f64-5717-4562-b3fc-2c963f66afa6resultobjectrequiredcurl -X POST "$BASE_URL/selfcustody/wallet/resync" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"addressId": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
"deep": true
}'{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"result": {
"$case": "success",
"success": {
"results": [
{
"addressId": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
"address": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
"ok": true,
"error": "string",
"syncedTillBlock": 0,
"ingestedTxCount": 0
}
]
}
}
}Add address
https://api.gobtcpay.com/public/api/v1.2/selfcustody/address/addRegisters an address the CLIENT derived and starts tracking it on-chain. The server verifies that address really derives from pubkeyHex and rejects the request otherwise, without that check a wallet could claim someone else's address. The call is idempotent (created: false on repeat) and only succeeds once the address is actually being tracked: an untracked address would silently report a zero balance.
Authorization: walletAuth
Body required
application/jsonaddressstringrequiredbc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdqpubkeyHexstringrequired02a1633cafcc01ebfb6d78e39f687a1f0995c62fc95f51ead10a02ee0be551b5dcderivationPathstringrequiredstringaddressIndexintegerrequired0ownershipChallengeIdstring3fa85f64-5717-4562-b3fc-2c963f66afa6ownershipSignaturestring4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7a…Responses
idstringrequired3fa85f64-5717-4562-b3fc-2c963f66afa6resultobjectrequiredcurl -X POST "$BASE_URL/selfcustody/address/add" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"address": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
"pubkeyHex": "02a1633cafcc01ebfb6d78e39f687a1f0995c62fc95f51ead10a02ee0be551b5dc",
"derivationPath": "string",
"addressIndex": 0,
"ownershipChallengeId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"ownershipSignature": "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"
}'{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"result": {
"$case": "success",
"success": {
"addressId": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
"address": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
"created": true,
"monitoredAt": "string",
"wasAlreadyMonitored": true
}
}
}Estimate fee
https://api.gobtcpay.com/public/api/v1.2/selfcustody/fee/estimateReturns recommended fee rates as three tiers (slow / standard / fast) in sat/vByte, sourced from a mempool estimator, with updatedAt telling how fresh they are, a cached rate is served when the source is unreachable, and its age is shown rather than hidden. Pass addressId AND amountSats together to also get exact: the fee for that address's real UTXO set, the inputCount and vBytes behind it, recipientAmountSats, and maxSendableSats. The last one is what a “Max” button should send, for BTC fee = feeRate × vBytes and vBytes depends on the input count, which only the server knows.
Authorization: walletAuth
Body required
application/jsonaddressIdstringbc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdqamountSatsstring | numberAmount in satoshis.
feeIncludedbooleantrueResponses
idstringrequired3fa85f64-5717-4562-b3fc-2c963f66afa6resultobjectrequiredcurl -X POST "$BASE_URL/selfcustody/fee/estimate" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"addressId": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
"amountSats": "string",
"feeIncluded": true
}'{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"result": {
"$case": "success",
"success": {
"slow": {
"feeRateSatVb": 50000,
"estimatedSecs": 0,
"exact": {
"feeSats": null,
"inputCount": 0,
"vBytes": 0,
"recipientAmountSats": null,
"maxSendableSats": null,
"sufficientFunds": true
}
},
"standard": {
"feeRateSatVb": 50000,
"estimatedSecs": 0,
"exact": {
"feeSats": null,
"inputCount": 0,
"vBytes": 0,
"recipientAmountSats": null,
"maxSendableSats": null,
"sufficientFunds": true
}
},
"fast": {
"feeRateSatVb": 50000,
"estimatedSecs": 0,
"exact": {
"feeSats": null,
"inputCount": 0,
"vBytes": 0,
"recipientAmountSats": null,
"maxSendableSats": null,
"sufficientFunds": true
}
},
"updatedAt": "string"
}
}
}Build unsigned transaction
https://api.gobtcpay.com/public/api/v1.2/selfcustody/transaction/buildBuilds an unsigned PSBT for the client to sign locally; the private key never reaches the server. Set feeIncluded: false to add the fee on top of amountSats, or true to take it out of that amount, recipientAmountSats reports what the recipient actually receives. Change below the dust threshold is added to the fee instead of creating an unspendable output. The selected outputs are LOCKED until expiresAt, so a concurrent build cannot pick the same ones; quoteId identifies both the quote and that lock.
Authorization: walletAuth
Body required
application/jsonaddressIdstringrequiredbc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdqtoAddressstringrequiredDestination Bitcoin address for the funds.
bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdqamountSatsstring | numberrequiredAmount in satoshis.
feeRateSatVbnumberrequiredFee rate in satoshis per virtual byte (sat/vB).
50000feeIncludedbooleanrequiredtrueResponses
idstringrequired3fa85f64-5717-4562-b3fc-2c963f66afa6resultobjectrequiredcurl -X POST "$BASE_URL/selfcustody/transaction/build" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"addressId": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
"toAddress": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
"amountSats": "string",
"feeRateSatVb": 50000,
"feeIncluded": true
}'{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"result": {
"$case": "success",
"success": {
"quoteId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"psbtBase64": "cHNidP8BAHECAAAAAfYI0kF1pQ0i3xL0AAAAAAD9////AsBdAQAAAAAAFgAUdR5",
"feeSats": "string",
"recipientAmountSats": "string",
"changeSats": "string",
"inputCount": 0,
"vBytes": 0,
"expiresAt": "string"
}
}
}Broadcast signed transaction
https://api.gobtcpay.com/public/api/v1.2/selfcustody/transaction/broadcastSubmits the transaction the client signed and finalized, rawTx is signed transaction hex, not a PSBT, and releases the lock on the outputs it spent. Only an unexpired quoteId is accepted; past expiresAt the quote must be rebuilt. Retrying with the same rawTx is safe and returns wasAlreadySubmitted: true: the network may have accepted the first attempt while the response never reached the client.
Authorization: walletAuth
Body required
application/jsonquoteIdstringrequired3fa85f64-5717-4562-b3fc-2c963f66afa6rawTxstringrequiredstringResponses
idstringrequired3fa85f64-5717-4562-b3fc-2c963f66afa6resultobjectrequiredcurl -X POST "$BASE_URL/selfcustody/transaction/broadcast" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"quoteId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"rawTx": "string"
}'{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"result": {
"$case": "success",
"success": {
"txid": "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b",
"wasAlreadySubmitted": true
}
}
}List transactions
https://api.gobtcpay.com/public/api/v1.2/selfcustody/transaction/listReturns transaction history newest-first with a nextCursor for the next page (null on the last one); transactions not yet in a block come first. Amounts are stated from the wallet's side: for an incoming transaction what arrived, for an outgoing one what went to recipients excluding our own change. feeSats is set only on outgoing transactions, the spender pays the fee, and may be null when not all inputs are known to the indexer, since reporting zero there would be wrong. Filter with statuses and directions.
Authorization: walletAuth
Body required
application/jsonaddressIdstringbc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdqcursorstringstringlimitinteger20statusesenum[]directionsenum[]Responses
idstringrequired3fa85f64-5717-4562-b3fc-2c963f66afa6resultobjectrequiredcurl -X POST "$BASE_URL/selfcustody/transaction/list" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"addressId": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
"cursor": "string",
"limit": 20,
"statuses": [
"pending"
],
"directions": [
"in"
]
}'{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"result": {
"$case": "success",
"success": {
"txs": [
{
"txid": "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b",
"addressId": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
"direction": "in",
"status": "pending",
"amountSats": "string",
"feeSats": null,
"counterpartyAddresses": [
"bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"
],
"blockHeight": 845000,
"blockTime": "string",
"confirmations": 0
}
],
"nextCursor": "string"
}
}
}Get transaction
https://api.gobtcpay.com/public/api/v1.2/selfcustody/transaction/getReturns one transaction by txid, in the same shape as the list entries, including confirmations. Use it to follow a send you just broadcast without refetching the whole history, and so that a dropped websocket cannot leave a transfer stuck on “processing” forever.
Authorization: walletAuth
Body required
application/jsontxidstringrequiredOn-chain Bitcoin transaction id (hex).
4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33bResponses
idstringrequired3fa85f64-5717-4562-b3fc-2c963f66afa6resultobjectrequiredcurl -X POST "$BASE_URL/selfcustody/transaction/get" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"txid": "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"
}'{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"result": {
"$case": "success",
"success": {
"tx": {
"txid": "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b",
"addressId": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
"direction": "in",
"status": "pending",
"amountSats": "string",
"feeSats": "string",
"counterpartyAddresses": [
"bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"
],
"blockHeight": 845000,
"blockTime": "string",
"confirmations": 0
}
}
}
}