← 返回 API 文档

Webhooks

Webhooks

配置回调地址、校验 HMAC 签名,并用幂等逻辑安全处理各类事件。

Webhooks

链上到账、账本状态变化时,Doggie Pay 会向你配置的地址推送 Webhook。

要不要把订单标为已支付、要不要发货,请以 Webhook 为准,不要只看浏览器跳转或前端轮询。

可先读:快速开始 · 用 Checkout 收款

1. 配置回调地址

打开 merchant.doggiepay.com:

  1. 填入 HTTPS Webhook URL(接受 POST,并读取原始 JSON 正文)
  2. 保存页面上只展示一次的 webhook_secret(whsec_…)
  3. 之后可以轮换密钥;轮换后,旧签名会校验失败
门户 / 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 时,系统会按退避策略重试
HeaderX-DoggiePay-Signature: t=<unix>,v1=<hex>
Content-Typeapplication/json

事件正文结构

{
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "type": "checkout.paid",
  "data": { }
}
字段类型说明
iduuid事件 id,用来做幂等
typestring事件类型
dataobject具体业务字段

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.paidCheckout 会话链上确认到账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 / failedCheckout(Payment)退款

5. 处理时建议怎么做

  1. 先验签,再跑业务逻辑
  2. 用 event.id(以及常用的 payment_id / deposit_id)做幂等——同一事件可能会重试
  3. 事件已经落库(或稳妥入队)之后,再返回 2xx
  4. HTTP 处理尽量短,耗时任务放到队列
  5. 不要把 webhook_secret 暴露给浏览器
  6. 轮换密钥时,线上配置要一起换成新 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