如何通过 webhook 自动确认订单付款
将发票与订单关联,验证 Webhook 签名,并防止重复事件、付款不足和回滚导致错误交付。
更新日期:
要让网站自动将订单标记为已付款,请在 Cryptanio 为其创建发票,并接入 webhook,即服务在付款状态变化时向你的服务器发送的通知。服务器验证消息真伪,找到订单并核对发票状态后,即可为买家开通付费访问权限或将订单交付履行。
下面以使用美元作为计算单位创建账单、由买家在付款页面选择可用加密货币和网络的商店为例。资金进入商店的地址。订单与商品交付仍由商店应用管理,Cryptanio 将发票与转账关联,并通知其状态。该接入需要开发者参与,或具备修改网站服务器代码的条件。
准备项目和付款地址
在 Cryptanio 控制台选择项目,添加属于自己的收款地址,并连接到 Payments。普通地址监控不足以用于发放发票。确认地址已激活且属于所选网络。真实付款需要使用正式运行模式的地址。
如果客户可能同时付款,应提前准备多个地址。地址可能正被未结发票占用,也可能在发票关闭后暂时不可用。没有空闲地址时,应用应显示清晰错误,并允许稍后重试。
创建 API 密钥并保存到商店服务器,用于调用 https://api.cryptanio.com/v1。不得将密钥放入网页 JavaScript、移动应用或公开代码仓库。
设置通知接收地址
打开所选项目的“通知”。如果已启用“使用账户通知设置”,请通过“账户设置”配置 Webhook,或关闭继承并设置项目独立接收地址。
在 Webhook 卡片的“结束点 URL”中填写处理器的 HTTPS 地址,点击“保存”。处理器必须直接接收 POST 请求,不能通过重定向转到其他 URL。在同一卡片获取“签名密钥”,并在服务器上与 API 密钥分开保存。
确认 Webhook 未暂停。在处理器中处理 payment.confirmed 和 payment.rolled_back,如需中间状态,可使用 payment.pending。履行订单前,请检查真实模式及下文其他条件。无需为账单地址单独启用 Webhook 渠道。
为具体订单创建发票
先将订单保存到商店数据库,再由服务器调用 POST /v1/invoices 创建发票。在 reference 中传入自己的订单标识。本例订单价格为 12.50 USD:invoice_currency 和 invoice_amount 指定价格,买家在付款页面选择可用网络和资产。因此,该请求不传入 chain、asset、amount、address 和 tolerance。使用 USD 不意味着接收法定货币,也不意味着将收到的加密货币兑换成美元。
#!/usr/bin/env bash
set -euo pipefail
: "${CRYPTANIO_API_KEY:?Set CRYPTANIO_API_KEY}"
: "${CRYPTANIO_OPERATION_ID:?Set CRYPTANIO_OPERATION_ID}"
: "${CRYPTANIO_PROJECT_ID:?Set CRYPTANIO_PROJECT_ID}"
json_string() {
local text="$1" char code index output='"'
for ((index=0; index<${#text}; index++)); do
char="${text:index:1}"
case "$char" in
'"'|'\') output+="\\$char" ;;
*) printf -v code '%d' "'$char"
if ((code < 32)); then printf -v char '\\u%04x' "$code"; fi
output+="$char" ;;
esac
done
printf '%s"' "$output"
}
response_file="$(mktemp)"
trap 'rm -f "$response_file"' EXIT
transfer_status=0
http_status="$(curl \
--silent --show-error --max-time 30 --request POST \
--header "Authorization: Bearer $CRYPTANIO_API_KEY" \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header "Idempotency-Key: $CRYPTANIO_OPERATION_ID" \
--data-binary '{"project":'"$(json_string "$CRYPTANIO_PROJECT_ID")"',"invoice_currency":"USD","invoice_amount":"12.50","reference":"order-42"}' \
"https://api.cryptanio.com/v1/invoices" \
--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
运行前,在服务器设置 CRYPTANIO_API_KEY、CRYPTANIO_PROJECT_ID 和 CRYPTANIO_OPERATION_ID。使用具有发票创建权限的正式 API 密钥;CRYPTANIO_PROJECT_ID 应指向已准备好的 Payments 项目。CRYPTANIO_OPERATION_ID 是为该订单保存的创建操作标识,由 16 至 80 个拉丁字母、数字、连字符或下划线组成。示例通过 Idempotency-Key 请求头传入它,而不是 JSON 字段。新订单应生成不同值,重试同一请求则保留原值。
创建成功后,API 返回 HTTP 201。将发票 id 和返回的 checkout_url 与订单一起保存,并引导买家打开该链接。选择付款方式前,网络、地址和加密货币金额尚未确定。买家在付款页面选择可用方式,获取当前收款信息和付款期限。如果在自己的界面显示收款信息,应使用具体付款尝试的数据、amount 中的准确金额和 expires_at 截止时间;不要四舍五入,也不要用 asked 替换金额。后续 GET 请求和发票列表中的链接仅供查看。创建响应中的可交互付款链接应单独保存,只交给对应买家。
如果发送请求后连接中断,使用相同的 Idempotency-Key 重发相同正文。成功重试将返回 HTTP 201 和同一个 id。使用已用过的密钥修改参数,会得到 HTTP 409。reference 用于关联发票与订单,但不能代替保证单次创建的幂等键。不要仅因超时就更换键值。
验证收到的 webhook
Cryptanio 会发送 X-Timestamp、X-Signature、X-Event-Id、X-Event-Type 和 X-Attempt。签名计算方式如下:
hex(HMAC-SHA256(secret, timestamp + "." + raw_body))
raw_body 是请求的原始字节,应在解析 JSON 前取得。重新序列化可能改变空格或字段顺序,导致验证失败。限制允许的正文大小,并拒绝没有有效签名的请求。
下面示例将时间有效窗口设为五分钟。这是接收处理器自己的配置,正常工作依赖服务器时钟同步。
import hashlib
import hmac
import re
import time
def verify_signature(secret, timestamp, raw_body, signature):
if not secret or not timestamp or not re.fullmatch(r"[0-9]{1,12}", timestamp):
return False
if not signature or not re.fullmatch(r"[0-9a-f]{64}", signature):
return False
if abs(time.time() - int(timestamp)) > 300:
return False
signed = timestamp.encode("ascii") + b"." + raw_body
expected = hmac.new(secret.encode("utf-8"), signed,
hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
该函数验证签名及请求时间。验证成功后再解析 JSON,检查 schema_version="v1"、预期的 project_id、mode="live" 和 sandbox=false。
业务判断应使用已签名正文中的 event_id 和 event_type。可以将同名请求头与正文对照,但这些请求头本身不受签名保护。只有预期的 payment 对象且方向为 in 时,才执行付款逻辑。
区分转账确认与订单付款完成
商店主要需要处理以下事件:
payment.pending:已检测到转账,仍在等待确认;payment.confirmed:转账达到所需确认数;payment.rolled_back:此前检测到的转账因区块链变化被撤回。
payment.confirmed 不代表某个订单已全额付款。检查 invoice 对象,将 invoice.id、invoice.reference 与保存的订单核对。未关联发票的消息,不能只因金额相似就自动归属于某次购买。
交付前调用 GET /v1/invoices/{id} 获取当前状态。核对 id、project_id、reference、invoice_currency 和 invoice_amount 中的订单价格,以及 sandbox=false。检查网络、资产、地址及所选付款尝试;存在多次尝试时,应考虑 settled_attempt_id 和 settled_attempt。成功付款对应发票状态 paid;pending、seen 和 partial 尚不允许交付。买家返回网站或打开付款链接,都不能确认付款。
不足付款使用 underpaid,等待补足的部分付款使用 partial,逾期到账使用 late。超额付款时,项目策略可能让发票保持 paid,也可能设置为带有 needs_decision=true 的 overpaid。underpaid、overpaid 和 late 都需要在履行订单前单独决定如何处理。amount_raw 和 paid 字段以字符串返回。比较时应将其解析为精确整数,单位为资产的最小单位;decimals 指定缩放比例。不要使用二进制浮点数。应提前制定差额处理规则;这并不意味着会自动退款。
处理重复投递和回滚
投递可能重复。将已验证事件保存到持久存储,使用 event_id 作为唯一键。已接收事件再次到达时,应返回成功响应,不得重复交付商品。
一笔转账会经历不同状态,因此只使用交易标识过滤所有后续消息并不足够。对于 chain、tx_id、index、address 和资产标识的组合,保存最近应用的 payment.revision。只有 revision 大于已处理值时,才应用状态转换。
订单修改与交付任务登记应一致提交,并防止并发重复处理。任务应具有订单唯一键。实际执行前,再次确认订单仍允许交付。收到回滚后应更新状态,停止尚未执行的交付;对已提供的访问权限,则准备复核或暂停流程。
检查完整流程
真实收款前,请用准备好的数据检查正文解析、签名和防重复处理。服务事件 webhook.test 不能确认购买,也不能替代完整流程检查。
然后用自己的验证订单,检查发票创建、转账、确认和订单更新的完整过程。使用准备好的数据,分别检查事件重复、不足付款和回滚。只有可靠保存已接收消息后才返回 HTTP 2xx;如果暂时无法保存,应返回服务器错误,让投递能够重试。
将控制台中的事件和投递日志与应用记录核对。只有确认订单恰好履行一次,而中间状态、其他订单或重复事件不会触发错误交付,检查才算完成。