Webhooks
Webhooks
配置回调地址、校验 HMAC 签名,并用幂等逻辑安全处理各类事件。
Webhooks
链上到账、账本状态变化时,Doggie Pay 会向你配置的地址推送 Webhook。
要不要把订单标为已支付、要不要发货,请以 Webhook 为准,不要只看浏览器跳转或前端轮询。
可先读:快速开始 · 用 Checkout 收款
1. 配置回调地址
- 填入 HTTPS Webhook URL(接受
POST,并读取原始 JSON 正文) - 保存页面上只展示一次的
webhook_secret(whsec_…) - 之后可以轮换密钥;轮换后,旧签名会校验失败
| 门户 / API | 说明 |
|---|---|
PUT /v1/merchant/webhook | 设置 { "url": "https://…" };明文 webhook_secret 只返回一次 |
POST /v1/merchant/webhook/rotate-secret | 轮换密钥(明文只返回一次) |
DELETE /v1/merchant/webhook | 清空 URL 与密钥 |
GET /v1/merchant/webhook/events | 查看投递记录(门户或 Secret) |
2. 投递规则
| 项目 | 约定 |
|---|---|
| 方法 | POST |
| 正文 | 原始 JSON 事件 |
| 成功 | 尽快返回 2xx(可以先落库,再异步处理) |
| 失败 | 非 2xx 时,系统会按退避策略重试 |
| Header | X-DoggiePay-Signature: t=<unix>,v1=<hex> |
| Content-Type | application/json |
事件正文结构
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"type": "checkout.paid",
"data": { }
}
| 字段 | 类型 | 说明 |
|---|---|---|
id | uuid | 事件 id,用来做幂等 |
type | string | 事件类型 |
data | object | 具体业务字段 |
3. 如何校验签名
签名头格式如下:
X-DoggiePay-Signature: t=1710000000,v1=abcdef…
计算方式:
v1 = HMAC-SHA256( webhook_secret, t + "." + rawBody )
请用常量时间比较核对 v1。出现下面情况应直接拒绝:
- 缺少签名头,或格式不对
|当前时间 - t|超出你设定的容差(例如 5 分钟),以防重放- 摘要对不上
Node.js 示例
import crypto from "node:crypto";
function verifyDoggiePaySignature(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(
String(header || "")
.split(",")
.map((p) => p.trim().split("="))
.filter((kv) => kv.length === 2),
);
const t = parts.t;
const v1 = parts.v1;
if (!t || !v1) return false;
const ts = Number(t);
if (!Number.isFinite(ts)) return false;
if (Math.abs(Date.now() / 1000 - ts) > toleranceSec) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`, "utf8")
.digest("hex");
const a = Buffer.from(expected, "utf8");
const b = Buffer.from(v1, "utf8");
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}
// Express:该路由请用 express.raw({ type: "application/json" })
app.post("/webhooks/doggiepay", (req, res) => {
const raw = req.body.toString("utf8");
if (!verifyDoggiePaySignature(raw, req.get("X-DoggiePay-Signature"), process.env.DOGGIEPAY_WEBHOOK_SECRET)) {
return res.status(401).send("invalid signature");
}
const event = JSON.parse(raw);
// TODO: 按 event.id 做幂等处理
res.status(200).json({ received: true });
});
Go 示例
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(t + "." + string(rawBody)))
expected := hex.EncodeToString(mac.Sum(nil))
ok := hmac.Equal([]byte(expected), []byte(v1))
务必对请求的原始字节做 HMAC。先 JSON.parse 再 stringify,很容易算错签名。
4. 事件一览
Checkout
type | 什么时候会收到 | data 里常见字段 |
|---|---|---|
checkout.paid | Checkout 会话链上确认到账 | payment_id、deposit_id、circle_tx_id |
checkout.expired | 未付款且已过期 | payment_id |
checkout.cancelled | 会话被取消 | payment_id |
这里的 payment_id,就是创建会话时返回的 id。
Customers(充值与提现)
type | 什么时候会收到 | data 里常见字段 |
|---|---|---|
deposit.credited | 长期地址入金,已记入商户资金池 | deposit_id、amount、currency、external_user_id、blockchain、tx_hash、circle_tx_id 等 |
withdrawal.prepared | 仅门户 Prepare 流程会发 | 提现相关字段 |
withdrawal.requested | 提现已受理 | … |
withdrawal.submitted | 已提交到链上或通道 | … |
withdrawal.complete | 提现成功 | … |
withdrawal.failed | 提现失败 | … |
external_user_id 就是你创建 Customer 时的 ref_id。收到 deposit.credited 后,请在你自己的系统给对应用户加余额;Doggie Pay 不维护 C 端用户余额。
商户出金与 Checkout 退款
type | 对应能力 |
|---|---|
payout.requested / submitted / complete / failed | 商户把自有资金提到自己的地址 |
refund.requested / submitted / complete / failed | Checkout(Payment)退款 |
5. 处理时建议怎么做
- 先验签,再跑业务逻辑
- 用
event.id(以及常用的payment_id/deposit_id)做幂等——同一事件可能会重试 - 事件已经落库(或稳妥入队)之后,再返回 2xx
- HTTP 处理尽量短,耗时任务放到队列
- 不要把
webhook_secret暴露给浏览器 - 轮换密钥时,线上配置要一起换成新 secret
处理 checkout.paid 的最小步骤
验签
→ 若 event.id 已处理过 → 直接 200
→ 把订单标为已支付(用 payment_id 或 metadata.order_id)
→ 记下 event.id
→ 返回 200
6. 自测时可以核对这些点
| 检查项 | 期望结果 |
|---|---|
| 故意传错签名 | 你的接口返回 401,且不产生业务副作用 |
同一 id 再推一次 | 第二次应视为已处理,不再重复记账 |
| 本地联调 | 用 HTTPS 隧道把公网回调指到本机 |
| 投递是否成功 | 在门户查看 GET /v1/merchant/webhook/events |
7. 接下来可以看什么
- 把 用 Checkout 收款 整条链路跑通,并真实收到
checkout.paid - 若要做 App 内钱包,再接入 Customers 的
deposit.credited
