文档中心 / Webhook 完全指南
PayRam Webhook 完全指南:签名校验、重试与幂等
支付状态变化时,PayRam 会向你的服务器发送 Webhook。这篇讲怎么接收、怎么确认它来自 PayRam,以及重复通知和漏掉的通知怎么处理。
先看结论:在后台登记一个 HTTPS 回调地址,PayRam 会在每次支付状态变化时向它 POST 一个 JSON。你的服务器要做三件事:用 X-Payram-Signature 校验签名,立刻返回 2xx,并按 reference_id 加 status 做幂等处理。
1. 在后台登记回调地址
- 进入后台的 设置 → Webhook(Settings → Webhook),点击 添加端点(Add Endpoint)。
- 填入你的回调地址,必须是能从公网访问的 HTTPS 地址,再写一段简短说明,保存。
- 把端点设为启用。PayRam 会把事件发送到该项目下每一个启用的端点。
每个项目有自己的 API 密钥和回调地址,开了多个项目的话要分别设置,详见 多项目管理。
2. 通知里有什么
请求是 POST,Content-Type: application/json。金额都是字符串,timestamp、confirmation_* 和 block_number 是数字。示例:
{
"customer_id": "1234",
"invoice_id": "INV-0090",
"reference_id": "a1b2c3d4e5",
"status": "FILLED",
"amount": "323.53",
"currency": "USDT",
"filled_amount": "323.53",
"filled_amount_in_usd": "323.53",
"timestamp": 1750340282,
"payment_info": [
{
"source_address": "0x7a2C…9bF1",
"transaction_hash": "0xabc123…def456",
"destination_address": "0x21d4cF2E…EA8d45",
"block_number": 21897412
}
],
"confirmation_current": 12,
"confirmation_required": 12
}
| 字段 | 含义 |
|---|---|
reference_id | PayRam 给这笔支付的唯一编号,用作幂等键 |
invoice_id | 你创建支付时传入的订单号,原样返回,用来对账 |
customer_id | 你系统里的客户编号 |
status | 支付状态:OPEN、PARTIALLY_FILLED、FILLED、OVER_FILLED、CANCELLED,详见 支付状态详解 |
amount / currency | 请求的金额与币种 |
filled_amount | 已收到的金额,检测到付款前可能是 null |
payment_info | 链上付款详情(来源地址、交易哈希、收款地址、区块高度),有链上付款之前为空 |
confirmation_current / confirmation_required | 当前确认数与所需确认数,例如 3 / 12。最终的已关闭或已取消通知里为 0 |
在链上确认期间,你还会收到带进度的通知,可以用来在页面上显示“确认中 5 / 12”。
3. 校验签名(务必做)
回调地址是公开的,任何人都可以向它发请求。所以必须确认请求真的来自 PayRam。官方推荐用请求头 X-Payram-Signature:
- 算法:HMAC-SHA256。
- 被签名的内容:请求体的原始字节。
- 密钥:你的项目 API 密钥。
- 格式:
sha256=<十六进制摘要>。
最常见的错误:把请求体解析成 JSON 再重新序列化后去算签名。字段顺序、空格只要有一点不同,签名就对不上。必须用收到的原始字节计算。
请求里还可能带有 API-KEY 头,内容是你的项目 API 密钥原文。这是为了向后兼容保留的旧方式,新代码请使用签名。
4. 完整示例:Node.js 与 Python
Node.js(Express)
关键是用 express.raw 拿到原始请求体,而不是 express.json。
const express = require('express');
const crypto = require('crypto');
const app = express();
const API_KEY = process.env.PAYRAM_API_KEY;
function verify(rawBody, signatureHeader) {
const expected =
'sha256=' + crypto.createHmac('sha256', API_KEY).update(rawBody).digest('hex');
const a = Buffer.from(signatureHeader || '');
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.post('/payram/webhook', express.raw({ type: 'application/json' }), (req, res) => {
if (!verify(req.body, req.get('X-Payram-Signature'))) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString('utf8'));
// 先快速确认收到,再异步处理,避免超时触发重试
res.sendStatus(200);
handleEvent(event).catch(console.error);
});
app.listen(3000);
Python(Flask)
import hmac, hashlib, os
from flask import Flask, request, abort
app = Flask(__name__)
API_KEY = os.environ["PAYRAM_API_KEY"].encode()
@app.post("/payram/webhook")
def payram_webhook():
raw = request.get_data() # 原始字节,不要用 request.json
sent = request.headers.get("X-Payram-Signature", "")
expected = "sha256=" + hmac.new(API_KEY, raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sent, expected):
abort(401)
event = request.get_json()
# 把事件放进队列,由后台任务处理,这里立刻返回
enqueue(event)
return "", 200
API 密钥请放在环境变量里,只保存在你的服务器上,不要写进网页或手机 App。
5. 重试规则:哪些情况会重发
- 成功的标准:你的服务器返回
2xx。返回 400 及以上,或超过 60 秒没有响应,都算失败并触发重试。 - 确认进度通知:每个周期最多重试 3 次(立即、2 秒后、4 秒后),并会在下一次轮询时继续发送,直到支付被付满。
- 最终通知(已关闭或已取消):先快速重试 3 次(立即、2 秒、4 秒),之后进入长期重试:30 分钟、1 小时、2 小时、4 小时、8 小时、24 小时、48 小时。第七次长期重试之后标记为失败,PayRam 不再发送。
所以你的服务器宕机一小段时间不会丢通知。但如果长时间不可用,最终通知可能已经用完重试次数,这时需要用状态查询接口补查,见下一节。
6. 幂等处理与对账
同一个状态可能被发送多次,比如你的服务器返回得太慢、触发了重试。因此处理逻辑必须幂等:同一条通知处理一次和处理十次,结果一样。
- 用
reference_id(或你自己的invoice_id)加status作为唯一键。 - 收到通知时先查:这个组合处理过吗?处理过就直接返回
200,不要再发货或加余额。 - 没处理过就在同一个数据库事务里“记录这个组合”并“更新订单”。数据库里给这个组合加唯一约束,可以挡住并发的重复请求。
不要只靠 Webhook。把它当作“通知你去看一眼”的信号,订单关键状态再用状态接口确认一次,并定期对账:把你系统里还没完成的订单,逐个用 reference_id 查询当前状态。具体做法见 API 对接教程。
7. 常见问题
签名一直对不上,是什么原因?
九成是因为没有用原始字节。检查三件事:用的是不是 express.raw 或 request.get_data();密钥是不是这个项目的 API 密钥(多个项目各有自己的密钥);请求头是不是完整比较了 sha256= 前缀。
我可以只校验 API-KEY 头吗?
可以,但那是旧方式。官方推荐使用 X-Payram-Signature,它还能保证请求体没有被改动。
收到 FILLED 就可以发货吗?
FILLED 表示已收到的金额等于请求的金额,并且已经达到所需的确认数。对大多数商家,这就是发货的信号。少付和多付怎么办,见 支付状态详解。
测试时回调地址在本机怎么办?
回调地址必须是公网可访问的 HTTPS 地址。本机开发时可以用隧道工具把本地端口临时暴露出来,测试完记得把后台里的端点改成正式地址。
用官方 SDK 能省事吗?
可以。官方 TypeScript / JavaScript SDK 为 Express、Fastify 和 Next.js 提供了现成的 Webhook 处理函数,但它默认检查的是旧的 API-Key 头,需要签名校验的话仍要自己补。详见 SDK 入门。
相关阅读
- API 创建支付:用 PayRam API 给自己的网站接加密货币收款:创建支付拿到付款链接,把客户引到支付页,用 Webhook 和状态接口确认到账,并用订单号对账。附 curl、Node.js、Python 示例。
- 支付状态详解:PayRam 的五个支付状态是什么意思,少付(PARTIALLY_FILLED)、多付(OVER_FILLED)和过期(CANCELLED)该怎么处理,以及如何用 Webhook 和状态接口把订单状态对准。
- TypeScript SDK:用官方 TypeScript / JavaScript SDK 对接 PayRam:安装、初始化客户端、创建支付、查询状态、接收 Webhook(Express、Fastify、Next.js),以及它的校验方式和你该补什么。
