Source: https://cryptanio.com/zh/wiki/webhook-zidong-queren-dingdan-fukuan/
Language: zh

# 如何通过 webhook 自动确认订单付款

将发票与订单关联，验证 Webhook 签名，并防止重复事件、付款不足和回滚导致错误交付。

更新日期: 2026-11-14

要让网站自动将订单标记为已付款，请在 Cryptanio 为其创建[发票](<https://cryptanio.com/zh/wiki/shenme-shi-zhangdan/>)，并接入 [webhook](<https://cryptanio.com/zh/wiki/shenme-shi-webhook/>)，即服务在付款状态变化时向你的服务器发送的通知。服务器验证消息真伪，找到订单并核对发票状态后，即可为买家开通付费访问权限或将订单交付履行。

下面以使用美元作为计算单位创建账单、由买家在付款页面选择可用加密货币和网络的商店为例。资金进入商店的地址。订单与商品交付仍由商店应用管理，Cryptanio 将发票与转账关联，并通知其状态。该接入需要开发者参与，或具备修改网站服务器代码的条件。

## 准备项目和付款地址 {#section-bc4b4e413f237d37}

在 Cryptanio 控制台选择项目，添加属于自己的收款地址，并连接到 Payments。普通地址监控不足以用于发放发票。确认地址已激活且属于所选网络。真实付款需要使用正式运行模式的地址。

如果客户可能同时付款，应提前准备多个地址。地址可能正被未结发票占用，也可能在发票关闭后暂时不可用。没有空闲地址时，应用应显示清晰错误，并允许稍后重试。

创建 API 密钥并保存到商店服务器，用于调用 ` https://api.cryptanio.com/v1 `。不得将密钥放入网页 JavaScript、移动应用或公开代码仓库。

## 设置通知接收地址 {#section-10882ac31f8fd58a}

打开所选项目的“通知”。如果已启用“使用账户通知设置”，请通过“账户设置”配置 Webhook，或关闭继承并设置项目独立接收地址。

在 Webhook 卡片的“结束点 URL”中填写处理器的 HTTPS 地址，点击“保存”。处理器必须直接接收 POST 请求，不能通过重定向转到其他 URL。在同一卡片获取“签名密钥”，并在服务器上与 API 密钥分开保存。

确认 Webhook 未暂停。在处理器中处理 ` payment.confirmed ` 和 ` payment.rolled_back `，如需中间状态，可使用 ` payment.pending `。履行订单前，请检查真实模式及下文其他条件。无需为账单地址单独启用 Webhook 渠道。

## 为具体订单创建发票 {#section-2e9b44d1558f4131}

先将订单保存到商店数据库，再由服务器调用 ` POST /v1/invoices ` 创建发票。在 ` reference ` 中传入自己的订单标识。本例订单价格为 12\.50 USD：` invoice_currency ` 和 ` invoice_amount ` 指定价格，买家在付款页面选择可用网络和资产。因此，该请求不传入 ` chain `、` asset `、` amount `、` address ` 和 ` tolerance `。使用 USD 不意味着接收法定货币，也不意味着将收到的加密货币兑换成美元。

```bash
#!/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
```

[下载示例](</wiki/examples/ace23bd7c3cba0e47dafdcd6075a506ddb2ec8d6581c6129af815c1558f5a8fc.sh>)

运行前，在服务器设置 ` 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 {#webhook}

Cryptanio 会发送 ` X-Timestamp `、` X-Signature `、` X-Event-Id `、` X-Event-Type ` 和 ` X-Attempt `。签名计算方式如下：

```text
hex(HMAC-SHA256(secret, timestamp + "." + raw_body))
```

` raw_body ` 是请求的原始字节，应在解析 JSON 前取得。重新序列化可能改变空格或字段顺序，导致验证失败。限制允许的正文大小，并拒绝没有有效签名的请求。

下面示例将时间有效窗口设为五分钟。这是接收处理器自己的配置，正常工作依赖服务器时钟同步。

```python
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 ` 时，才执行付款逻辑。

## 区分转账确认与订单付款完成 {#section-e57f5e45468376d6}

商店主要需要处理以下事件：

- ` 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 ` 指定缩放比例。不要使用二进制浮点数。应提前制定差额处理规则；这并不意味着会自动退款。

## 处理重复投递和回滚 {#section-5c5260f8e065d36d}

投递可能重复。将已验证事件保存到持久存储，使用 ` event_id ` 作为唯一键。已接收事件再次到达时，应返回成功响应，不得重复交付商品。

一笔转账会经历不同状态，因此只使用交易标识过滤所有后续消息并不足够。对于 ` chain `、` tx_id `、` index `、` address ` 和资产标识的组合，保存最近应用的 ` payment.revision `。只有 revision 大于已处理值时，才应用状态转换。

订单修改与交付任务登记应一致提交，并防止并发重复处理。任务应具有订单唯一键。实际执行前，再次确认订单仍允许交付。收到回滚后应更新状态，停止尚未执行的交付；对已提供的访问权限，则准备复核或暂停流程。

## 检查完整流程 {#section-0f4baa174f389712}

真实收款前，请用准备好的数据检查正文解析、签名和防重复处理。服务事件 ` webhook.test ` 不能确认购买，也不能替代完整流程检查。

然后用自己的验证订单，检查发票创建、转账、确认和订单更新的完整过程。使用准备好的数据，分别检查事件重复、不足付款和回滚。只有可靠保存已接收消息后才返回 HTTP 2xx；如果暂时无法保存，应返回服务器错误，让投递能够重试。

将控制台中的事件和投递日志与应用记录核对。只有确认订单恰好履行一次，而中间状态、其他订单或重复事件不会触发错误交付，检查才算完成。

参考：[API v1 发票](<https://cryptanio.com/ru/docs.html#invoices>)、[webhook 签名验证](<https://cryptanio.com/ru/docs.html#verify>)。

标签: [付款](https://cryptanio.com/zh/wiki/tag/payments/), [Webhooks](https://cryptanio.com/zh/wiki/tag/webhooks/)

## 相关文章

- [什么是付款账单](https://cryptanio.com/zh/wiki/shenme-shi-zhangdan/)
- [什么是 webhook](https://cryptanio.com/zh/wiki/shenme-shi-webhook/)
- [如何在 Telegram 机器人中接受加密货币付款](https://cryptanio.com/zh/wiki/crypto-payments-telegram-bot/)
