SHKeeper v2.5.32 API 使用详解

SHKeeper v2.5.32 REST API 全量实测详解:四种鉴权逐一验证、36 条路由分组 curl 实跑回显,覆盖余额/报价/收款/发票/地址/交易/提现/钱包/设置/回调全端点,含 HMAC-SHA256 回调验签复算、HTTP 状态码实测汇总与十二则排障 FAQ。

SHKeeper加密货币支付系统自托管2026/10/24

SHKeeper v2.5.32 API 使用详解

目标:把 SHKeeper v2.5.32 的 REST API 一次讲全——四种鉴权凭据逐一实测、36 条 /api/v1/* 路由按业务分组逐条 curl 实跑、错误形态与 HTTP 状态码实测汇总、商户回调报文的 HMAC-SHA256 验签复算,以及十二则实测排障 FAQ。全部请求与回显于 2026-10-24 在 《SHKeeper v2.5.32 Docker Compose 部署指南》 搭建的 BTC regtest 实测栈上逐条执行采集;产品定位与架构背景见 《SHKeeper v2.5.32 介绍与选型指南》,环境变量与配置语义见 《SHKeeper v2.5.32 配置参数详解》。

一、实测环境与接口面总览

1.1 实测环境

项值说明
实测日期2026-10-24全部请求 curl 实跑,回显逐字照抄
SHKeeper 核心vsyshost/shkeeper:2.5.32Python 3.13 / Flask / Gunicorn,服务监听 5000,宿主入口 8128
BTC 适配器vsyshost/bitcoin-shkeeper:2.0.14HTTP 服务监听 6000,另有 Celery 任务进程
BTC 全节点Bitcoin Core v31.1-regtest 模式,RPC 端口 18443
管理员账号admin / q1w2e3r4登录管理台与 Session、Basic、Metrics 共用
API KeyNjOwsKx8v5Kctk9aY0O20w商户业务接口凭据,请求头 X-Shkeeper-API-Key
Backend Keyq1w2e3r4节点侧回调凭据,请求头 X-Shkeeper-Backend-Key,核心与适配器两侧一致
汇率设定USD/BTC manual 100000,费率 2%另设 EUR/CNY dynamic(Binance 源,实测环境外网不可达)
回调接收器http://callback-receiver:9000/hook部署指南中的回调接收服务,验证回调并返回 202

下文 curl 统一用变量承载入口与凭据:

export SK=http://127.0.0.1:8128
export API=NjOwsKx8v5Kctk9aY0O20w
export BK=q1w2e3r4

1.2 接口面构成:36 条路由 + /metrics

v2.5.32 核心的 API 全部集中在 shkeeper/api_v1.py,共注册 36 条 /api/v1/* 路由,外加一条独立的 /metrics 监控端点。按鉴权方式分五组:

鉴权组路由数代表端点
免鉴权1GET /api/v1/crypto
API Key(X-Shkeeper-API-Key)15crypto/balances、payment_request、quote、balance、invoices、payout/status、addresses、transactions、tx-info、payouts、fee-deposit-address、decryption-key、test-callback-receiver
Session(管理台 Cookie)14payment-gateway(GET/POST)、payment-gateway/token、settings/<key>(GET/POST)、status、server、backup、exchange-rate、estimate-tx-fee、generate-address、transaction、payout_destinations、autopayout
HTTP Basic(可回退 Session)3payout、multipayout、task/<task_id>
Backend Key(X-Shkeeper-Backend-Key)3walletnotify/<币种>/<txid>、payoutnotify/<币种>、<币种>/decrypt

💡 两个容易踩坑的接口面事实:其一,源码引入了 flask_smorest 并为多数端点挂了 OpenAPI 文档装饰器,但 OPENAPI_URL_PREFIX 未在配置中启用,生产实例没有 /api-docs Swagger 页面——接口契约只能靠源码或本文这类实测清单获得;其二,路由按 <币种> 参数化(如 /api/v1/BTC/...),只有 *_WALLET 环境变量启用为 enabled 的币种才有实例(本文栈只启用 BTC),调用未启用币种返回网关不可用(见 6.4)。

1.3 统一响应形态与两条集成铁律

业务响应是统一的 JSON 信封:成功 {"status":"success", ...},失败 {"status":"error","message":"..."}。实测得出两条集成铁律:

  1. HTTP 状态码不可靠:无 API Key、错 API Key、参数缺失、业务异常等绝大多数错误都返回 HTTP 200 + error JSON(第十二章实测汇总),集成方必须解析 body 的 status 字段做判断,不能只看状态码;
  2. 错误 message 可能直接是 Python 异常名:如缺字段返回 {"message":"'fee'","status":"error"}(KeyError 的字符串形态),个别错误还带 traceback 字段,对接日志告警时注意识别。

二、鉴权体系:四种凭据逐一实测

四种凭据对应三类调用方:商户后端用 API Key,管理员/浏览器用 Session(打款另可用 Basic),区块链节点侧用 Backend Key。

2.1 免鉴权:GET /api/v1/crypto

币种能力发现接口,无需任何凭据:

curl -s $SK/api/v1/crypto

实测回显:

{"crypto":["BTC"],"crypto_list":[{"display_name":"Bitcoin","name":"BTC"}],"status":"success"}

crypto 是币种代码数组,crypto_list 携带显示名,适合做商户后台的币种下拉与可用性探测。

2.2 X-Shkeeper-API-Key:商户业务接口(15 条路由)

覆盖余额、报价、收款、发票、地址、交易、打款状态查询、钱包解密提交与回调测试等全部对外业务面。请求头不区分大小写(HTTP 头语义),本文统一写 X-Shkeeper-Api-Key。三个失败分支实测:

# 分支一:不带 key
curl -s $SK/api/v1/BTC/addresses
# 分支二:key 错误
curl -s -H 'X-Shkeeper-Api-Key: wrongkey' $SK/api/v1/BTC/addresses
# 分支三:拿 API Key 访问 Session 组端点
curl -s -H "X-Shkeeper-Api-Key: $API" $SK/api/v1/BTC/status

实测回显(三者 HTTP 状态码均为 200):

{"message":"No API key","status":"error"}
{"message":"Bad API key","status":"error"}
{"message":"This endpoint doesn't accept X-Shkeeper-Api-Key auth","status":"error"}

⚠️ 第三条与第二条 body 形态不同但都无 HTTP 错误码,若集成代码只判 HTTP 200 就把错误响应当成功解析,会在运行期才爆 JSON 字段缺失。务必先读 status 字段。

2.3 Session Cookie:管理台与运维端点(14 条路由)

payment-gateway、settings、status、backup、exchange-rate、generate-address 等管理/运维端点要求登录会话。登录是标准表单 POST,字段名是 name 和 password:

curl -si -d 'name=admin&password=q1w2e3r4' $SK/login | head -6

实测回显:

HTTP/1.1 302 FOUND
Server: gunicorn
Date: Sat, 24 Oct 2026 10:56:14 GMT
Connection: keep-alive
Content-Type: text/html; charset=utf-8
Content-Length: 201

302 即登录成功(首次部署未设密码时先走 /set-password,已设密码后登录完成钱包解锁链路,见部署指南第三、四章)。之后携带 Cookie 访问受保护端点(本文实测使用会话文件):

curl -s -b cookies.txt $SK/api/v1/BTC/status

无凭据访问 Session 组端点的表现:

curl -si $SK/api/v1/BTC/status | head -6

实测回显:

HTTP/1.1 302 FOUND
Server: gunicorn
Date: Sat, 24 Oct 2026 10:50:32 GMT
Connection: keep-alive
Content-Type: text/html; charset=utf-8
Content-Length: 199
<!doctype html>
<html lang=en>
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to the target URL: <a href="/login">/login</a>. If not, click the link.

⚠️ 两个实测结论:其一,HTTP Basic 不能替代 Session——curl -u admin:q1w2e3r4 $SK/api/v1/BTC/status 同样得到 302 跳 /login(Basic 只在打款异步端点生效,见 2.4);其二,登录表单字段名用错(如 login=/username=)直接得到 HTTP/1.1 400 BAD REQUEST,不是错误 JSON。

2.4 HTTP Basic:打款异步端点(3 条路由)

POST /api/v1/<币种>/payout、POST /api/v1/<币种>/multipayout、GET /api/v1/<币种>/task/<task_id> 三条打款链路端点用 basic_auth_optional + login_required 双装饰器:带 Basic 凭据则校验之,不带则回退 Session。凭据错误时的实测回显:

curl -s -u admin:wrongpass -H 'Content-Type: application/json' \
  -X POST $SK/api/v1/BTC/payout \
  -d '{"destination":"bcrt1qm4y0nf7cv0mhqjvm6shh57q7vhk6atjmz0rjtn","amount":"0.00002","fee":"2"}'
{"message":"Bad HTTP Basic Auth credentials","status":"error"}

(该分支 HTTP 状态码同样为 200。)

2.5 X-Shkeeper-Backend-Key:节点侧回调(3 条路由)

walletnotify、payoutnotify、<币种>/decrypt 是给区块链节点侧进程调用的:bitcoind 的 -walletnotify 脚本在新交易入块时回调核心,适配器解锁状态上报也走这一组。凭据通过请求头 X-Shkeeper-Backend-Key 传递,值必须与两侧 SHKEEPER_BACKEND_KEY 一致。缺头与错值的分支实测见 11.1(返回 403 + 错误 JSON)。

2.6 /metrics:Prometheus 指标端点

独立于 /api/v1 之外的监控端点,HTTP Basic 认证(管理员账号)。无凭据:

curl -s $SK/metrics

实测回显(HTTP 401):

{"msg":"authorization requred","status":"error"}

💡 requred 为服务端响应原文拼写(源码如此),做告警规则匹配时别按正确拼写写。

带凭据的完整输出(本栈实测):

curl -s -u admin:q1w2e3r4 $SK/metrics
# HELP bitcoin_fullnode_status Connection status to bitcoin fullnode
# TYPE bitcoin_fullnode_status gauge
bitcoin_fullnode_status 1.0
# HELP bitcoin_fullnode_last_block Last block loaded to the bitcoin fullnode
# TYPE bitcoin_fullnode_last_block gauge
bitcoin_fullnode_last_block 109.0
# HELP bitcoin_wallet_last_block Last checked wallet block
# TYPE bitcoin_wallet_last_block gauge
bitcoin_wallet_last_block 108.0
# HELP bitcoin_fullnode_last_block_timestamp Fullnode block timestamp
# TYPE bitcoin_fullnode_last_block_timestamp gauge
bitcoin_fullnode_last_block_timestamp 1.792831002e+09
# HELP bitcoin_wallet_last_block_timestamp Wallet block timestamp
# TYPE bitcoin_wallet_last_block_timestamp gauge
bitcoin_wallet_last_block_timestamp 0.0
# HELP bitcoin_shkeeper_status Connection status to bitcoin_shkeeper
# TYPE bitcoin_shkeeper_status gauge
bitcoin_shkeeper_status 1.0

bitcoin_fullnode_last_block 与 bitcoin_wallet_last_block 的差值就是适配器扫块滞后量,是支付到账延迟排查的第一指标(实测中曾观察到钱包游标落后全节点 2 个块,见 11.1 的扫块时序说明)。

2.7 鉴权速查表

凭据传递方式适用路由失败表现(实测)
无—GET /api/v1/crypto—
API Key请求头 X-Shkeeper-API-Key15 条业务路由HTTP 200 + No API key / Bad API key
SessionPOST /login(表单 name/password)取 Cookie14 条管理路由302 跳 /login;API Key 冒用返回不接受提示
HTTP Basic-u admin:password3 条打款链路(可回退 Session)HTTP 200 + Bad HTTP Basic Auth credentials
Backend Key请求头 X-Shkeeper-Backend-Key3 条节点回调路由403 + No backend key provided / Wrong backend key
Basic(Metrics)-u admin:passwordGET /metrics401 + authorization requred

三、加密币与余额查询

3.1 全量余额:GET /api/v1/crypto/balances

API Key 组,返回所有启用币种的余额、法币估值与汇率:

curl -s -H "X-Shkeeper-Api-Key: $API" $SK/api/v1/crypto/balances

实测回显:

[{"amount_crypto":"0.00018615","amount_fiat":"18.615","display_name":"Bitcoin","fiat":"USD","name":"BTC","rate":"100000","server_status":"Synced"}]

支持 ?includes=BTC 按币种过滤,单币种时输出与全量相同(实测确认)。server_status 非 Synced 时余额与汇率不可信,应停止开票。

3.2 单币余额:GET /api/v1/BTC/balance

curl -s -H "X-Shkeeper-Api-Key: $API" $SK/api/v1/BTC/balance

实测回显:

{"amount_crypto":"0.00018615","amount_fiat":"18.615","display_name":"Bitcoin","fiat":"USD","name":"BTC","rate":"100000.0000000000","server_status":"Synced"}

💡 精度差异:单币端点的 rate 是 100000.0000000000(10 位小数),全量列表是 100000。做金额解析时按字符串高精度处理(Python 用 Decimal),不要转 float。

3.3 钱包状态:GET /api/v1/BTC/status

Session 组(注意与 3.2 的 API Key 组区分):

curl -s -b cookies.txt $SK/api/v1/BTC/status

实测回显:

{"amount":"0.00018615","name":"BTC","server":"Synced"}

3.4 手续费归集地址:GET /api/v1/BTC/fee-deposit-address

curl -s -H "X-Shkeeper-Api-Key: $API" $SK/api/v1/BTC/fee-deposit-address

实测回显:

{"crypto":"BTC","fee_deposit_address":"","status":"success"}

BTC 适配器未启手续费归集账户时返回空串,属正常现象(该端点主要服务于支持手续费账户的币种,如 TRON 系);集成方应对空串做兼容,不要直接拿去生成收款码。

四、支付网关开关与共享令牌

4.1 查询网关状态与令牌:GET /api/v1/BTC/payment-gateway

Session 组:

curl -s -b cookies.txt $SK/api/v1/BTC/payment-gateway

实测回显:

{"enabled":true,"status":"success","token":"NjOwsKx8v5Kctk9aY0O20w"}

token 就是该币种的 API Key(商户请求头用值)。

4.2 启停网关:POST /api/v1/BTC/payment-gateway

curl -s -b cookies.txt -H 'Content-Type: application/json' \
  -X POST $SK/api/v1/BTC/payment-gateway -d '{"enabled":true}'

实测回显:

{"status":"success"}

置 false 即停收:商户侧所有 payment_request 会得到 payment gateway is unavailable(见 6.4),已开发票的查询与回调不受影响。

4.3 重置共享令牌:POST /api/v1/BTC/payment-gateway/token

curl -s -b cookies.txt -H 'Content-Type: application/json' \
  -X POST $SK/api/v1/BTC/payment-gateway/token -d '{"token":"NjOwsKx8v5Kctk9aY0O20w"}'

实测回显:

{"status":"success"}

💡 该端点设置的 token 是所有钱包共享的全局值(对多币种实例,一次设置全部生效);实测传回原值幂等成功。轮换密钥时注意:旧 Key 立即失效,商户端需同步更新,否则全部业务请求转 Bad API key。

五、报价与汇率

5.1 询价:POST /api/v1/BTC/quote

不生成发票的纯换算(API Key 组),金额已含费率:

curl -s -H "X-Shkeeper-Api-Key: $API" -H 'Content-Type: application/json' \
  -X POST $SK/api/v1/BTC/quote -d '{"fiat":"USD","amount":"100"}'

实测回显:

{"amount_crypto":"0.00102000","amount_fiat":"100","crypto":"BTC","exchange_rate":"100000.0000000000","fiat":"USD","status":"success"}

100 USD 按汇率 100000 应为 0.001 BTC,返回 0.00102 正是上浮 2% 费率后的应收值。缺参分支:

curl -s -H "X-Shkeeper-Api-Key: $API" -H 'Content-Type: application/json' \
  -X POST $SK/api/v1/BTC/quote -d '{}'
{"message":"'fiat' and 'amount' are required fields.","status":"error"}

dynamic 源币种在外网不可达环境下的实测表现(CNY 走 Binance 源,输出节选,实测 message 首行):

curl -s -H "X-Shkeeper-Api-Key: $API" -H 'Content-Type: application/json' \
  -X POST $SK/api/v1/BTC/quote -d '{"fiat":"CNY","amount":"100"}'
{"message":"HTTPSConnectionPool(host='api.binance.com', port=443): Max retries exceeded with url: /api/v3/ticker/price?symbol=BTCCNY (Caused by NewConnectionError('<urllib3.connection.HTTPSConnection object at 0x7aca6279d590>: Failed to establish a new connection: [Errno 101] Network is unreachable'"

💡 内网/离线部署(regtest、隔离机房)必须把汇率源切为 manual(见 5.2),dynamic 源不可达会直接导致开票失败。

5.2 写入手动汇率:POST /api/v1/BTC/exchange-rate

Session 组,写 manual 汇率与费率(与本栈既定值一致,幂等):

curl -s -b cookies.txt -H 'Content-Type: application/json' \
  -X POST $SK/api/v1/BTC/exchange-rate \
  -d '{"fiat":"USD","source":"manual","rate":"100000","fee":"2"}'

实测回显:

{"status":"success"}

5.3 链上费率估算:GET /api/v1/BTC/estimate-tx-fee/0.0001

Session 组,按金额估算转账矿工费:

curl -s -b cookies.txt $SK/api/v1/BTC/estimate-tx-fee/0.0001

实测回显:

{"msg":"Provider call failed for 'estimatefee': bitcoind: -32601: Method not found","status":"error"}

这是确定性失败:Bitcoin Core v31.1 已移除 estimatefee RPC,适配器调用即报 -32601,该端点在 31.1 节点下恒定不可用(影响与规避见 FAQ ②)。v2.5.32 的收款与打款主链路不依赖它,打款费率由请求显式传 fee。

六、收款请求与发票

6.1 创建收款发票:POST /api/v1/BTC/payment_request

商户集成的核心端点(API Key 组)。external_id 是商户侧订单号,callback_url 是回调地址,必填:

curl -s -H "X-Shkeeper-Api-Key: $API" -H 'Content-Type: application/json' \
  -X POST $SK/api/v1/BTC/payment_request \
  -d '{"external_id":"9002","fiat":"USD","amount":"1.00","callback_url":"http://callback-receiver:9000/hook"}'

实测回显:

{"amount":"0.0000102","display_name":"Bitcoin","exchange_rate":"100000","id":3,"recalculate_after":0,"status":"success","wallet":"bcrt1q44vceewtd3h6zt63meg4zzx5pxplkqaanyjltx"}

字段解读:wallet 是为该发票派生的收款地址;amount 是含费后的应收币量(1.00 USD × 1.02 ÷ 100000 = 0.0000102 BTC);recalculate_after 为发票汇率重算秒数(0 表示不重算,由 <币种>_RECALCULATE_AFTER 类环境变量控制,见配置详解)。缺 callback_url 的实测分支(HTTP 200,traceback 节选):

curl -s -H "X-Shkeeper-Api-Key: $API" -H 'Content-Type: application/json' \
  -X POST $SK/api/v1/BTC/payment_request \
  -d '{"external_id":"TEST-NOCB2","fiat":"USD","amount":"1"}'
{"message":"'callback_url'","status":"error","traceback":"Traceback (most recent call last):\n  File \"/shkeeper.io/shkeeper/api_v1.py\", line 133, in payment_request\n    invoice = Invoice.add(crypto=crypto, request=req)\n  File \"/shkeeper.io/shkeeper/models.py\", line 427, in add\n    callback_url=request[\"callback_url\"],\n                 ~~~~~~~^^^^^^^^^^^^^^^^\nKeyError: 'callback_url'\n"}

幂等性实测:同一 external_id 重复请求不新建发票,而是返回同一 id 与同一收款地址(实测用 external_id=2001 重复创建,两次均返回 id:2 与同一地址)——商户重试逻辑可以放心基于 external_id 做幂等。

6.2 查询发票:GET /api/v1/invoices/<external_id> 与 GET /api/v1/invoices

单笔查询(本节 6.1 的发票在支付入账后):

curl -s -H "X-Shkeeper-Api-Key: $API" $SK/api/v1/invoices/9002

实测回显:

{"invoices":[{"amount_fiat":"1","balance_fiat":"1.02","external_id":"9002","fiat":"USD","status":"OVERPAID","txs":[{"addr":"bcrt1q44vceewtd3h6zt63meg4zzx5pxplkqaanyjltx","amount":"0.0000102","crypto":"BTC","status":"CONFIRMED","txid":"0199c41bc9ee34e4a2ae99539ad8d120b141b18ed1085d9c4dfcefdc33cec547"}]}],"status":"success"}

全量查询(实测时点栈内两条发票):

curl -s -H "X-Shkeeper-Api-Key: $API" $SK/api/v1/invoices
{"invoices":[{"amount_fiat":"18.25","balance_fiat":"18.615","external_id":"1001","fiat":"USD","status":"PAID","txs":[{"addr":"bcrt1qtsag9mtsgdr5etcxmswank0jmvddmzyyedzj8r","amount":"0.00018615","crypto":"BTC","status":"CONFIRMED","txid":"42e4f277fe55b1d3a479ff1d15dca3e28faa7e524489d917e8246448072f4418"}]},{"amount_fiat":"29.9","balance_fiat":"0","external_id":"2001","fiat":"USD","status":"UNPAID","txs":[]}],"status":"success"}

查询不存在的 external_id 返回空列表而非报错:

{"invoices":[],"status":"success"}

⚠️ 两笔足额支付、两种终态(实测对比,集成必读):1001(18.25 USD)与 9002(1.00 USD)都是「按 payment_request 返回的 amount 足额支付」,1001 终态 PAID(回执 amount_fiat_without_fee 为 18.25,费额扣减后与订单额精确相等),9002 终态 OVERPAID(balance_fiat 1.02 大于订单额 1)。两例的差异说明费额入账比对在 v2.5.32 中并非全路径一致,商户侧不要假设 amount 精确等于到账值:以回调/轮询得到的 status 状态机为准(PAID/OVERPAID/PARTIAL 均按到账处理,金额差异走对账),并以 /invoices/<external_id> 轮询兜底回调丢失。

6.3 发票状态机(实测观察)

状态含义实测触发
UNPAID未收到链上入账创建后初始态(2001)
PARTIAL部分支付多笔累计未达额(回调报文 paid:false,见部署指南回调章节)
PAID达到应收额1001:含费足额支付入块确认后
OVERPAID超过应收额9002:足额含费支付但比对口径为费前毛额
EXPIRED / CANCELED过期 / 取消管理台操作与超时(部署指南发票生命周期章节)

6.4 未启用币种的请求:POST /api/v1/ETH/payment_request

本栈 ETH_WALLET=disabled,对其调用业务端点:

curl -s -H "X-Shkeeper-Api-Key: $API" -H 'Content-Type: application/json' \
  -X POST $SK/api/v1/ETH/payment_request \
  -d '{"external_id":"x","fiat":"USD","amount":"1","callback_url":"http://127.0.0.1:9000/h"}'

实测回显:

{"message":"ETH payment gateway is unavailable","status":"error"}

与配置详解 2.1 节的开关语义互证:disabled 币种无实例可路由,统一网关不可用错误。

七、地址与交易

7.1 列出收款地址:GET /api/v1/BTC/addresses

curl -s -H "X-Shkeeper-Api-Key: $API" $SK/api/v1/BTC/addresses

实测回显:

{"addresses":["bcrt1qtsag9mtsgdr5etcxmswank0jmvddmzyyedzj8r","bcrt1qm4y0nf7cv0mhqjvm6shh57q7vhk6atjmz0rjtn","bcrt1qj0mfqtqveqhlz9f66v35fpn6mv6htfqv3g2qm3","bcrt1q44vceewtd3h6zt63meg4zzx5pxplkqaanyjltx"],"status":"success"}

四条地址分别是发票 1001、2001、generate-address 生成、发票 9002 派生的地址,可对账核对。

7.2 按地址查交易:GET /api/v1/transactions/BTC/<地址>

已收款地址:

curl -s -H "X-Shkeeper-Api-Key: $API" \
  $SK/api/v1/transactions/BTC/bcrt1qtsag9mtsgdr5etcxmswank0jmvddmzyyedzj8r

实测回显:

{"status":"success","transactions":[{"addr":"bcrt1qtsag9mtsgdr5etcxmswank0jmvddmzyyedzj8r","amount":"0.00018615","crypto":"BTC","status":"CONFIRMED","txid":"42e4f277fe55b1d3a479ff1d15dca3e28faa7e524489d917e8246448072f4418"}]}

未收款地址返回空数组(实测 2001 的地址):

{"status":"success","transactions":[]}

7.3 交易详情:GET /api/v1/tx-info/<txid>/<external_id>

curl -s -H "X-Shkeeper-Api-Key: $API" \
  $SK/api/v1/tx-info/42e4f277fe55b1d3a479ff1d15dca3e28faa7e524489d917e8246448072f4418/1001

实测回显:

{"info":{"addr":"bcrt1qtsag9mtsgdr5etcxmswank0jmvddmzyyedzj8r","amount":"18.615","crypto":"BTC"},"status":"success"}

注意路径参数顺序是 txid 在前、external_id 在后,且该接口查的是 SHKeeper 记账视角(amount 为法币入账额 18.615),不是链上原始交易。

7.4 手动登记交易:POST /api/v1/BTC/transaction

Session 组端点,供管理台场景把链上交易手工补录进 SHKeeper 记账(body 为交易对象)。自动化对接一般用不到,排查「回调丢失 + 扫块滞后」时可配合 11.1 的 walletnotify 重放使用。

八、提现:Payout 与 Multipayout

打款链路是异步任务模型:提交(立即返回 task_id)→ Celery 执行 → 轮询任务与打款状态。

8.1 单笔打款:POST /api/v1/BTC/payout

Basic 或 Session 认证,body 四要素 destination/amount/fee/external_id(callback_url 可选,用于打款完成回调):

curl -s -u admin:q1w2e3r4 -H 'Content-Type: application/json' \
  -X POST $SK/api/v1/BTC/payout \
  -d '{"destination":"bcrt1qm4y0nf7cv0mhqjvm6shh57q7vhk6atjmz0rjtn","amount":"0.00002","fee":"2","external_id":"PO-3002","callback_url":"http://callback-receiver:9000/hook"}'

实测回显:

{"external_id":"PO-3002","task_id":"7f788915-5d2d-44a1-b7da-ef97449bfc47"}

三个失败分支实测:

{"message":"Bad HTTP Basic Auth credentials","status":"error"}
{"message":"'fee'","status":"error"}
{"message":"'destination'","status":"error"}

⚠️ 第三个分支是实测踩坑:请求键写 dest 会得到 {"message":"'destination'","status":"error"}——v2.5.32 单笔打款路径只认全拼 destination(dest 仅在部分配置路径兼容),且 fee 必填( 错误 message 就是 KeyError 的字段名字符串)。

8.2 任务轮询:GET /api/v1/BTC/task/<task_id>

curl -s -u admin:q1w2e3r4 $SK/api/v1/BTC/task/7f788915-5d2d-44a1-b7da-ef97449bfc47

实测回显(regtest 地址不被 bitcoinlib 识别,任务失败):

{"result":"Address bcrt1qm4y0nf7cv0mhqjvm6shh57q7vhk6atjmz0rjtn is not valid address","status":"FAILURE"}

这正好复现了部署指南排障实录的第 14 条:regtest 的 bcrt1 地址在适配器的 bitcoinlib 校验下不被认作合法地址,打款任务必然 FAILURE。主网/testnet 地址无此问题;regtest 环境验证打款链路时应预期失败在此环节(状态机、鉴权、任务轮询均已验证到执行期)。

8.3 打款状态查询:GET /api/v1/BTC/payout/status?external_id=

API Key 组,商户按自己的 external_id 查询(注意与 8.2 的 task 轮询是两条独立状态线):

curl -s -H "X-Shkeeper-Api-Key: $API" "$SK/api/v1/BTC/payout/status?external_id=PO-3002"

实测回显:

{"amount":"0.0000200000","crypto":"BTC","destination":"bcrt1qm4y0nf7cv0mhqjvm6shh57q7vhk6atjmz0rjtn","external_id":"PO-3002","id":15,"status":"IN_PROGRESS","txid":null}

实测还观察到终态 FAIL 的历史记录(external_id=PO-1003:"status":"FAIL","txid":null),即记录状态会随任务结果推进;但 8.1 的地址校验失败任务其记录停留在 IN_PROGRESS,两条状态线不完全同步,对账以 payout/status 为准并辅以 8.2 的任务结果定位失败原因。缺 external_id 参数时返回 {"error":"external_id is required"}。

8.4 按金额查询打款记录:GET /api/v1/BTC/payouts

curl -s -H "X-Shkeeper-Api-Key: $API" "$SK/api/v1/BTC/payouts?amount=0.0001"

实测回显:

{"status":"success"}

不带参数的实测回显(HTTP 200,traceback 节选):

{"message":"No amount provided.","status":"error","traceback":"Traceback (most recent call last):\n  File \"/shkeeper.io/shkeeper/api_v1.py\", line 774, in list_payouts\n    raise Exception(\"No amount provided.\")\nException: No amount provided.\n"}

amount 必填;该端点语义是「是否存在该金额的打款记录」,主要供节点侧脚本核对打款批次。

8.5 批量打款:POST /api/v1/BTC/multipayout

body 为打款请求数组(元素字段与单笔一致):

curl -s -u admin:q1w2e3r4 -H 'Content-Type: application/json' \
  -X POST $SK/api/v1/BTC/multipayout \
  -d '[{"destination":"bcrt1q9x8z7v6u5w4t3s2r1q0p9o8n7m6l5k4j3h2g1f","amount":"0.00005","fee":"2","external_id":"MPO-2001"}]'

实测回显:

{"external_ids":["MPO-2001"],"task_id":"c5c4b602-9391-4594-9ae0-cb55d2edcecc"}

注意与单笔的差异:响应是 external_ids 复数数组,对应批量里每个元素的 external_id(重复值会在提交期被拒绝)。任务轮询同样走到地址校验失败:

{"result":"Address bcrt1q9x8z7v6u5w4t3s2r1q0p9o8n7m6l5k4j3h2g1f is not valid address","status":"FAILURE"}
curl -s -H "X-Shkeeper-Api-Key: $API" "$SK/api/v1/BTC/payout/status?external_id=MPO-2001"
{"amount":"0.0000500000","crypto":"BTC","destination":"bcrt1q9x8z7v6u5w4t3s2r1q0p9o8n7m6l5k4j3h2g1f","external_id":"MPO-2001","id":12,"status":"IN_PROGRESS","txid":null}

💡 官方 README 明确 BTC 不支持 multipayout(仅部分币种适配器实现),实测表现是提交期受理、执行期报错——集成方不要靠「提交成功」判断支持度,以各币种官方说明为准。

8.6 提现目标与自动提现:payout_destinations 与 autopayout

Session 组。列出提现目标:

curl -s -b cookies.txt -H 'Content-Type: application/json' \
  -X POST $SK/api/v1/BTC/payout_destinations -d '{"action":"list"}'

实测回显:

{"payout_destinations":[],"status":"success"}

autopayout 策略非法值的实测回显:

curl -s -b cookies.txt -H 'Content-Type: application/json' \
  -X POST $SK/api/v1/BTC/autopayout -d '{"policy":"INVALID"}'
{"message":"Unknown payout policy: INVALID","status":"error"}

自动提现策略按管理台界面可选值传 policy,非法值在提交期即被拒绝(不会进入任务队列)。

九、钱包管理

9.1 生成地址:GET /api/v1/BTC/generate-address

Session 组,主动派生一个新收款地址(进 7.1 的地址池):

curl -s -b cookies.txt $SK/api/v1/BTC/generate-address

实测回显:

{"addr":"bcrt1qj0mfqtqveqhlz9f66v35fpn6mv6htfqv3g2qm3","status":"success"}

9.2 全钱包导出:GET /api/v1/BTC/backup

Session 组,返回全部派生地址的私钥/公钥 JSON(输出节选,私钥字段值已隐去):

curl -s -b cookies.txt $SK/api/v1/BTC/backup
{
    "bcrt1q06kt5ha7xjnv60yz3fqwxvrkuhkazj6vcggp97": {
        "private": "c153…(已隐去)",
        "public": "02c0…(已隐去)",
        "public_address": "bcrt1q06kt5ha7xjnv60yz3fqwxvrkuhkazj6vcggp97",
        "wif": "zprv…(已隐去)"
    },
    "bcrt1q2hydwzp6qw0vldtw78yacqccqq845hldg0aald": {
        …
    }
}

⚠️ 这是明文私钥全量导出,等效于钱包完全失窃面:接口在 Session 组内(有登录态即可调),务必确保管理入口有 TLS 与双因素(部署指南生产化建议),导出文件离线加密保存,且不要把该端点暴露给任何自动化系统。

9.3 钱包解密状态:GET /api/v1/BTC/decrypt 与 POST /api/v1/decryption-key

decrypt(Backend Key 组)供适配器侧查询加密与解锁状态:

curl -s -H "X-Shkeeper-Backend-Key: $BK" $SK/api/v1/BTC/decrypt

实测回显:

{"key":"q1w2e3r4","persistent_status":"enabled","runtime_status":"success"}

persistent_status:enabled 表示钱包加密已启用,runtime_status:success 表示运行期已解锁,key 回显当前解密键(仅节点侧内网可信环境可调)。商户侧提交解密键走 POST /api/v1/decryption-key(API Key 组,form 表单字段 key,不是 JSON):

curl -s -H "X-Shkeeper-Api-Key: $API" -X POST $SK/api/v1/decryption-key -d 'key=whatever'

实测回显(已解锁态,无论键值对错):

{"message":"Decryption key was already entered","status":"success"}

不带 key 字段的实测回显:

{"message":"Decryption key is required","status":"error"}

源码逻辑(api_v1.py decryption_key):运行期已解锁直接返回 already entered(先于键值校验);持久态 enabled 时用 test_key 校验,对错分别返回 {"status":"success"} 与 Invalid decryption key;未加密返回 Wallet is not encrypted。钱包加密三态机制见配置详解第四章。

十、系统设置

10.1 设置读写:GET/POST /api/v1/settings/<key>

Session 组,白名单机制:v2.5.32 源码中 PUBLIC_SETTINGS 仅含 start_banner_shown 一个键,白名单外的读写一律 403:

curl -s -b cookies.txt $SK/api/v1/settings/start_banner_shown

实测回显(写入 1 之后):

{"key":"start_banner_shown","status":"success","value":"1"}
curl -si -b cookies.txt $SK/api/v1/settings/SECRET_KEY | head -1

实测回显(body 为 {"message":"forbidden","status":"error"}):

HTTP/1.1 403 FORBIDDEN
curl -s -b cookies.txt -H 'Content-Type: application/json' \
  -X POST $SK/api/v1/settings/start_banner_shown -d '{"value":1}'
{"status":"success"}

💡 大多数运行期设置(汇率、费率、币种开关、回调开关等)并不通过该端点暴露,而是各自业务端点或环境变量(见配置详解二、三章);settings API 仅用于管理台横幅这类 UI 态。

10.2 未实现的端点:GET /api/v1/BTC/server

curl -si -b cookies.txt $SK/api/v1/BTC/server | head -1

实测回显(body 为 {"code":500,"status":"Internal Server Error"}):

HTTP/1.1 500 INTERNAL SERVER ERROR

确定性 500:BTC 适配器类未实现 get_rpc_credentials(api_v1.py 直接调用即抛异常),该端点只对内置直连 RPC 凭据管理的币种有意义。对 BTC 栈看到 500 不是故障,属版本边界。

十一、回调通知与 HMAC-SHA256 验签实测

11.1 节点侧回调:walletnotify 与 payoutnotify

bitcoind 通过 -walletnotify 脚本在新交易入块时回调 POST /api/v1/walletnotify/BTC/<txid>,打款完成由适配器回调 POST /api/v1/payoutnotify/BTC。两件事实测验证:

其一,必须是 POST。用 GET 触发:

curl -si $SK/api/v1/walletnotify/BTC/42e4f277fe55b1d3a479ff1d15dca3e28faa7e524489d917e8246448072f4418 | head -1

实测回显:

HTTP/1.1 405 METHOD NOT ALLOWED

其二,Backend Key 三分支(正确/错误/缺失):

curl -s -H "X-Shkeeper-Backend-Key: $BK" -X POST \
  $SK/api/v1/walletnotify/BTC/42e4f277fe55b1d3a479ff1d15dca3e28faa7e524489d917e8246448072f4418
{"status":"success"}
{"message":"Wrong backend key","status":"error"}
{"message":"No backend key provided","status":"error"}

错值与缺失均为 403。payoutnotify 同样带 Backend Key 提交即回 {"status":"success"}。

扫块时序实测(支付到账延迟的关键):适配器扫块游标只推进到 safe_latest_height(全节点最新高度减 1,防重组),Celery 周期扫块。实测流程——向发票 9002 地址支付并入块后,等了一个扫块周期发票仍 UNPAID(/metrics 显示钱包游标 109 落后全节点 110);此时手动重放 walletnotify 是明确的即时补账手段:对已确认交易重放上述端点(回 success),发票随即入账转 OVERPAID。生产排查「链上已确认但发票未动」时,按此顺序:先看 /metrics 游标差,再重放 walletnotify,最后核对回调接收日志。

11.2 商户回调报文

发票入账后核心向 callback_url POST 报文,接收方必须返回 202,否则每 60 秒重发。完整报文与状态机见部署指南 8.3 节(实测样本:发票 1001 的 PAID 回调,含 balance_crypto/balance_fiat/fee_policy/transactions[] 等)。两个安全要点(与部署指南回调要点一致):新版回调带 X-Shkeeper-Signature(HMAC-SHA256)与 X-Shkeeper-Timestamp 头,务必验签后再信任;旧版 X-Shkeeper-Api-Key 头仅为兼容保留。

11.3 HMAC-SHA256 验签复算(实测 MATCH)

签名机制(源码 shkeeper/services/webhook_hmac.py):密钥为该钱包的 API Key;被签内容是紧凑序列化的回调 body——json.dumps(payload, separators=(",", ":"), sort_keys=True, ensure_ascii=False);消息 = "{ts}." + body;验签允许时钟偏差 max_age_sec=300。用发票 1001 的真实回调(接收器日志 ts=1792831233、sig=2ca8dec4aff2066d8802d2ddbfcae69ca50e8ca69ca4ff8733e45db7d789bcf3)复算:

import json, hmac, hashlib

payload = {  # 回调报文(与接收器日志逐字段一致,含顶层 addr)
  "addr": "bcrt1qtsag9mtsgdr5etcxmswank0jmvddmzyyedzj8r",
  "balance_crypto": "0.00018615",
  "balance_fiat": "18.615",
  "crypto": "BTC",
  "external_id": "1001",
  "fee_fixed": "0",
  "fee_percent": "2",
  "fee_policy": "PERCENT_FEE",
  "fiat": "USD",
  "overpaid_fiat": "0.00",
  "paid": True,
  "status": "PAID",
  "transactions": [{
      "amount_crypto": "0.00018615", "amount_fiat": "18.615",
      "amount_fiat_without_fee": "18.25", "crypto": "BTC",
      "date": "2026-10-24 08:40:32", "fee_fiat": "0.365",
      "trigger": True,
      "txid": "42e4f277fe55b1d3a479ff1d15dca3e28faa7e524489d917e8246448072f4418"}],
}
ts, secret = 1792831233, b"NjOwsKx8v5Kctk9aY0O20w"
pretty  = json.dumps(payload, indent=2, ensure_ascii=False).encode()
compact = json.dumps(payload, separators=(",", ":"), sort_keys=True, ensure_ascii=False).encode()
for name, body in (("pretty", pretty), ("compact", compact)):
    sig = hmac.new(secret, f"{ts}.".encode() + body, hashlib.sha256).hexdigest()
    print(name, sig[:16], sig == "2ca8dec4aff2066d8802d2ddbfcae69ca50e8ca69ca4ff8733e45db7d789bcf3")

实测输出:

pretty beff1a5aba954bac False
compact 2ca8dec4aff2066d True

结论:紧凑序列化复算与请求头签名精确一致(MATCH),缩进序列化不匹配。验签实现照抄第 11.3 节开头四个要素即可;最常见的验签失败原因就是拿「展示用 pretty JSON」或未 sort_keys 的原始拼接去做摘要。接收端还应校验 abs(now - ts) <= 300 再比对摘要,防重放。

11.4 回调链路自测:POST /api/v1/test-callback-receiver

API Key 组,让核心把一条测试回调打到指定接收器,用于不发起真实支付就验证回调链路。空 body 会因 JSON 解析失败得到 400(实测):

{"code":400,"status":"Bad Request"}

带 body 实测:

curl -si -H "X-Shkeeper-Api-Key: $API" -H 'Content-Type: application/json' \
  -X POST $SK/api/v1/test-callback-receiver \
  -d '{"external_id":"TEST-CB","fiat":"USD","status":"PAID"}'

实测回显(HTTP 202):

{"message":"callback logged","status":"success"}
202

十二、错误处理与状态码实测汇总

全部错误分支逐一实测(HTTP 状态码 + body 实采):

场景HTTPbody 形态(实测)
无 API Key / 错 API Key200{"message":"No API key"/"Bad API key","status":"error"}
API Key 访问 Session 端点200{"message":"This endpoint doesn't accept X-Shkeeper-Api-Key auth","status":"error"}
Session 端点无凭据 / 用 Basic302跳转 /login 页
打款 Basic 凭据错误200{"message":"Bad HTTP Basic Auth credentials","status":"error"}
必填字段缺失(quote/fee/destination/callback_url 等)200{"message":"'<字段名>'","status":"error"}(payment_request 缺 callback_url 另带 traceback)
业务异常(dynamic 汇率不可达、地址校验失败、未知策略等)200{"message":"<异常信息>","status":"error"}
未启用币种200{"message":"<币种> payment gateway is unavailable","status":"error"}
payouts 缺 amount200{"message":"No amount provided.","status":"error","traceback":"…"}
walletnotify 缺/错 Backend Key403{"message":"No backend key provided"/"Wrong backend key","status":"error"}
walletnotify 用 GET405Flask 默认方法不允许
settings 白名单外键403{"message":"forbidden","status":"error"}
metrics 无凭据401{"msg":"authorization requred","status":"error"}
test-callback-receiver 空 body400{"code":400,"status":"Bad Request"}
BTC/server(未实现)500{"code":500,"status":"Internal Server Error"}

解读:SHKeeper 把「业务失败」几乎全部放在 HTTP 200 + status:error 里,HTTP 错误码只保留给协议层问题(方法、鉴权头、白名单、未实现)。集成代码的健壮性顺序应是:解析 JSON → 判 status → 才是 HTTP 码;同时对 message 里的 Python 异常字符串('fee'、'callback_url')做告警降噪。

十三、排障 FAQ(十二则实测踩坑)

① regtest 地址被打款任务拒绝:payout/multipayout 任务 FAILURE Address bcrt1q… is not valid address——适配器的 bitcoinlib 不识别 regtest bech32 地址(部署指南排障 14 同源)。regtest 只能验证打款链路前半段(鉴权、任务模型、状态查询),真实验打款用主网/testnet 小额。

② estimate-tx-fee 恒定报错:Provider call failed for 'estimatefee': bitcoind: -32601: Method not found——Bitcoin Core 31.1 移除了 estimatefee RPC。收款链路不依赖它;打款费率走请求里的 fee 字段。

③ payout 请求键名:必须是 destination(写 dest 得 {"message":"'destination'"}),amount/fee 必填,fee 为百分比数字(如 2 表示 2%)。

④ walletnotify 必须用 POST:GET 直接 405。bitcoind 侧 -walletnotify 脚本记得带 -X POST 与 Backend Key 头。

⑤ settings 端点 403:白名单只有 start_banner_shown,其余键一律 {"message":"forbidden"}。不要试图用该端点读写汇率/开关——走 5.2 的 exchange-rate 或环境变量。

⑥ BTC/server 500:BTC 适配器未实现 get_rpc_credentials,属版本确定性边界,非故障。

⑦ payment_request 缺 callback_url:HTTP 200 + KeyError traceback。商户 SDK 封装时把 callback_url 设为必填参数即可在客户端拦截。

⑧ dynamic 汇率源不可达:隔离网络部署 quote 报 HTTPSConnectionPool… Network is unreachable。切 manual 源(5.2);多币种实例注意逐币种配置。

⑨ 永远不要只看 HTTP 状态码:绝大多数错误是 200 + error JSON(第十二章)。 Webhook 侧同理——回调丢失时先轮询 /invoices/<external_id> 兜底。

⑩ 登录表单字段是 name:写 login/username 得 400 BAD REQUEST;Basic 又不能顶替 Session(302)。脚本自动化请先 curl -c cookies.txt -d 'name=…&password=…' /login 再带 Cookie。

⑪ 未启用币种统一「网关不可用」:ETH payment gateway is unavailable 并非网络故障,先查 <币种>_WALLET 开关与 GET /api/v1/crypto 能力列表。

⑫ 找不到 API 文档页:flask_smorest 已注册但 OpenAPI 前缀未启用,实例没有 /api-docs。以本文与源码 shkeeper/api_v1.py 为契约依据。

十四、参考与关联