如何不使用 webhook,通过 API 检查付款
通过 API 在服务器端检查账单:付款状态、有限轮询、错误处理、订单仅履约一次以及重启后的恢复。
更新日期:
无需 webhook,也可以从应用服务器检查付款:定期通过 Cryptanio API 读取账单,将其与订单核对,并在当前状态为 paid 时履行订单。这种定期查询称为轮询。它需要检查队列、错误处理以及防止重复履约的机制:仅有一次成功的 HTTP 响应并不能确认付款。
如果应用可以执行后台任务,但暂时不方便接收传入通知,可以采用这种方式。变化会在下一次读取时被发现;轮询不能提供即时通知。
需要什么
在自己的数据库中保存订单与账单的关联:账单的 id、项目、自定义的 reference、商定价格以及运行模式。标识符应保存为字符串。要检查的是该订单对应的账单,而不是某笔碰巧金额相似的交易。
准备一个服务器端 API 密钥,具有 invoices 读取权限,以及 payments 服务和所需项目的访问权限。真实订单需要使用相应的正式运行模式。不要把密钥放在浏览器、买家链接或公开日志中。独立的核对进程只需要只读权限。
第1步:获取当前状态
此示例读取一张账单。请在服务器上设置 CRYPTANIO_API_KEY 和 CRYPTANIO_RESOURCE_ID;后者是已保存的账单 id,不是订单编号,也不是 TXID。
#!/usr/bin/env bash
set -euo pipefail
: "${CRYPTANIO_RESOURCE_ID:?Set CRYPTANIO_RESOURCE_ID}"
: "${CRYPTANIO_API_KEY:?Set CRYPTANIO_API_KEY}"
urlencode() {
local LC_ALL=C text="$1" char index
for ((index=0; index<${#text}; index++)); do
char="${text:index:1}"
case "$char" in [a-zA-Z0-9.~_-]) printf '%s' "$char" ;; *) printf '%%%02X' "'$char" ;; esac
done
}
response_file="$(mktemp)"
trap 'rm -f "$response_file"' EXIT
transfer_status=0
http_status="$(curl \
--silent --show-error --max-time 30 --request GET \
--header "Authorization: Bearer $CRYPTANIO_API_KEY" \
--header 'Accept: application/json' \
"https://api.cryptanio.com/v1/invoices/$(urlencode "$CRYPTANIO_RESOURCE_ID")" \
--output "$response_file" --write-out '%{http_code}')" || transfer_status=$?
if [[ "$http_status" == 2?? ]]; then
cat "$response_file"
exit "$transfer_status"
fi
printf 'HTTP %s\n' "$http_status" >&2
cat "$response_file" >&2
exit 1
收到 HTTP 200 后,解析 JSON,并将 id、project_id、reference 和 sandbox 与自己的记录核对。真实订单应为 sandbox=false。如果曾通过 invoice_currency 和 invoice_amount 保存价格,也请检查这两个字段。不属于该订单的账单,或来自其他模式的记录,都不应改变订单。
GET 返回的 checkout_url 链接用于查看。不要用它替换创建账单时从响应中保存的交互式链接:读取状态既不会创建新的付款,也不会签发新的交互式 checkout。
第2步:区分等待和付款完成
根据账单的 status 作出判断:
pending:正在等待付款;seen:已检测到转账,但付款尚未完成;partial:部分金额已经确认,继续按照该付款的规则等待;underpaid、overpaid、late:需要排查,并考虑needs_decision和卖家的决定;paid:账单已记为付款,核实与订单的关联后,可以准备履约;expired、canceled:结束买家当前的付款等待,但保留记录,供后续核对。
到期不代表不可能稍后到账。人工决定和交易回滚也可能改变后续处理。不要把某个状态认定为永久最终状态,也不要在页面关闭后删除订单与账单的关联。
已确认的交易与已付款的账单不是一回事。确认次数并不能证明所需资产、金额和收款信息恰好属于这个订单。具体区别见订单仍未付款的检查指南。
第3步:考虑金额和付款尝试
更换付款方式时,同一张账单可能产生不同的付款尝试。对于已付款账单,请考虑 settled_attempt_id 和 settled_attempt。排查历史记录时,检查 attempt_history.has_more 和后续游标;所选尝试的转账记录使用 payment_history。第一页不一定包含完整历史。
amount 是应发送的准确加密货币金额。amount_raw 和 paid 是以资产最小单位表示的整数字符串,decimals 指定小数位数。不要按浮点数进行比较,也不要像普通字符串一样按字典顺序比较。同时,您自己的金额检查不能代替账单状态和项目规则:多付款项可能被接受,也可能被提交审核。未经检查条件,也不应自行将不足额的付款相加。
第4步:限制轮询和重试
将检查放入具有并发限制的服务器端队列。买家打开多个标签页不应产生多个独立的查询循环。请计算账户的整体请求量:不同 API 密钥和模式共享额度。当前限制见 API 参考文档。
根据自己的流程设置等待时间,为其他请求保留余量。遇到网络错误或 HTTP 500/503 时,使用有上限的重试,逐步增加等待时间,并加入随机抖动。遇到 HTTP 429 时降低整体请求量;如果收到 Retry-After,按指定时间暂停。不要因为缺少该响应头就立即重试。每个请求都应有超时限制,每组重试也应有上限;达到上限后仍保留任务,以便稍后排查。
HTTP 401/403 表示需要检查密钥和权限。遇到 404 时,检查 ID、项目和模式:访问限制可能隐藏账单。这些错误并不意味着客户没有支付任何款项。
第5步:持久化履约和恢复信息
状态为 paid 时,以保持一致性的方式保存订单状态和履约任务。使用唯一的订单键,并防止并行重复处理。再次读取到 paid 不应再次提供访问权限或商品;交付后的故障也不应产生第二次履约。
将最后一次成功检查和下一项任务保存在持久化存储中。重启后恢复未完成的检查和任务,而不是只在打开页面时才开始流程。根据业务流程的期限限制当前付款等待;独立的后台核对仍需考虑延迟付款、决定和回滚,包括此前已付款的订单。在尚未执行的交付之前,再次确认是否仍可履约;对已经提供的访问权限,也应安排重新评估。
使用预先准备的响应检查重复的 paid、超时、重启以及回滚后的状态转换。正确的结果是:订单只履行一次,不明确或存在争议的付款仍保留待查。之后可以在保留相同履约保护机制的前提下,用 webhook 补充轮询。