KukoPay
  • 产品能力
  • 快速接入
  • API 参考
  • 商户后台
KukoPay

面向出海业务的一站式支付基础设施。通过一套服务端 API 接入托管收银台、订单、退款、Webhook、钱包与提现。

© 2026 KukoPay. All rights reserved.

产品

  • 产品能力
  • 接入流程
  • 安全设计
  • 商户后台

开发者

  • 文档首页
  • 快速接入
  • 统一下单 API
  • Webhook

公司

  • 联系我们
  • 服务条款
  • 隐私政策
  • 可接受使用政策

开始

文档首页快速接入商户入驻与审核认证与环境支付流程

对接指南

异步通知 Webhook幂等与重试沙箱测试

资金

费率与结算拒付与争议提现出金

API 参考

统一下单订单查询申请退款拒付收款链接余额查询资金流水事件Webhook 端点API 调用日志

接口约定

列表、分页与导出metadata错误码
开发者文档
异步通知 Webhook

异步通知 Webhook

事件类型、HMAC-SHA256 验签、密钥轮换、幂等与失败重投。

账户上发生的每一件事都会记录成一个事件,再由 KukoPay 主动推送到你配置的地址。事件是既成事实,推送只是告诉你的一次尝试——这个区分很重要,它意味着即使推送全部失败,事件也不会丢,可以从 事件接口 补回来。

事件类型

事件含义
payment.succeeded支付成功、资金已入钱包,可以履约
payment.failed支付被通道判定为终态失败,不会再成功
payment.expired超过 24 小时无人支付,订单已关闭
payment.duplicate_refunded订单关闭后又收到买家付款,已全额退回买家,不影响商户余额(见 支付流程)
refund.succeeded退款已真实扣账完成
refund.failed退款被通道拒绝,冻结资金已退回可用余额
dispute.created买家发起拒付,争议金额已被冻结
dispute.updated拒付进入申诉审理阶段
dispute.won申诉成功,冻结资金已退回
dispute.lost申诉失败,争议金额已扣账
dispute.closed发卡行撤销拒付,冻结资金已退回
⚠️

只订阅成功事件是常见的坑:失败和过期事件不推送时,你只能靠轮询才能发现一笔订单已经死了。

事件结构

data.object 与对应查询接口返回的对象完全一致,所以一套解析代码同时服务于 Webhook 和主动查询。

{
  "object": "event",
  "id": "evt_3f2a9c7e1b4d0568a2cf",
  "type": "payment.succeeded",
  "mode": "live",
  "created_at": "2026-09-21T08:31:12.114Z",
  "data": {
    "object": {
      "object": "order",
      "trade_no": "TRD_9F3A2C7E...",
      "out_trade_no": "ORD_20260921_0001",
      "amount": 2990,
      "fee": 135,
      "net_amount": 2855,
      "currency": "USD",
      "status": "paid",
      "payment_method": "card",
      "refundable_until": null,
      "metadata": { "customer_id": "cus_42" },
      "paid_at": "2026-09-21T08:31:10.882Z"
    }
  }
}
ℹ️

顶层的 event_id、event_type 以及 data 里与 data.object 平级的那些扁平字段属于旧版结构,仅为兼容保留,新代码请一律读 id、type 和 data.object。

请求头

X-KukoPay-Event-Id: evt_3f2a9c7e1b4d0568a2cf
X-KukoPay-Event-Type: payment.succeeded
X-KukoPay-Endpoint-Id: whe_8c2f41ab9d0e7635
X-KukoPay-Signature: t=1758268800,v1=9c1f...

X-KukoPay-Endpoint-Id 只在事件发往你配置的端点时出现;使用订单级 notify_url 时没有这个头。

验签

签名内容是 HMAC-SHA256(secret, "{t}.{原始请求体}") 的十六进制结果。必须使用原始请求字节验签,不要先解析 JSON 再重新序列化。

🚨

X-KukoPay-Signature 里可能出现多个 v1=(密钥轮换窗口期内新旧密钥各签一次)。 只要有任意一个匹配就是合法请求。用 Object.fromEntries 之类的写法解析会只保留最后一个,导致轮换期间验签全部失败。

import crypto from "crypto"
 
const TOLERANCE_SECONDS = 300
 
export function verifyKukoPay(rawBody, signatureHeader, secret) {
  // 头里可能有多个 v1=,逐个收集
  const parts = signatureHeader.split(",").map((item) => {
    const index = item.indexOf("=")
    return [item.slice(0, index).trim(), item.slice(index + 1).trim()]
  })
 
  const timestamp = parts.find(([key]) => key === "t")?.[1]
  const received = parts.filter(([key]) => key === "v1").map(([, value]) => value)
  if (!timestamp || received.length === 0) return false
 
  // 拒绝过旧的时间戳,避免签名被重放
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_SECONDS) {
    return false
  }
 
  const expected = Buffer.from(
    crypto
      .createHmac("sha256", secret)
      .update(`${timestamp}.${rawBody}`)
      .digest("hex")
  )
 
  // 任意一个匹配即可:轮换窗口内新旧密钥会各签一次
  return received.some((candidate) => {
    const buffer = Buffer.from(candidate)
    return (
      buffer.length === expected.length &&
      crypto.timingSafeEqual(buffer, expected)
    )
  })
}

配置接收端点

一个环境下可以配置多个端点,每个端点有独立的签名密钥,并可以只订阅自己关心的事件类型。完整接口见 Webhook 端点。

curl -X POST "https://web.kukopay.com/api/v1/webhook_endpoints" \
  -H "X-Api-Key: kuko_live_你的密钥" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://api.yourshop.com/hooks/kukopay",
    "description": "订单履约服务",
    "enabled_events": ["payment.succeeded", "refund.succeeded"]
  }'

签名密钥只在创建和轮换时返回一次,之后无法再读回明文。

优先级

下单时传了 notify_url,该订单的事件只发往这个地址,不再发给已配置的端点。没有传时,事件发给所有订阅了该类型的端点。两者都没有时,回落到开发者中心配置的单一地址。

密钥轮换

调用轮换接口后,旧密钥会在 24 小时内继续与新密钥一起签名,两个签名出现在同一个 X-KukoPay-Signature 里。你可以在窗口期内任意时刻更新配置,不必和平台同步切换。

curl -X POST "https://web.kukopay.com/api/v1/webhook_endpoints/whe_xxx/rotate_secret" \
  -H "X-Api-Key: kuko_live_你的密钥"

重试策略

接收端在 10 秒内返回 2xx 表示已接收。其它状态或超时会按以下节奏重投,共 6 次:

30 秒 → 2 分钟 → 10 分钟 → 30 分钟 → 2 小时 → 6 小时

同一事件重投时 id 不变。多个端点各自独立重试,一个端点故障不会拖住其它端点。

推荐处理顺序

  1. 读取原始请求体。
  2. 验证签名,并拒绝超过 5 分钟的时间戳。
  3. 在数据库事务中插入唯一的事件 id。
  4. 执行业务状态变更。
  5. 尽快返回 2xx;耗时任务交给自己的队列。
⚠️

不要只在内存里去重,也不要先发货再记录事件。进程重启或并发通知会绕过这两种做法。

漏了怎么办

接收端宕机一整天、重试全部耗尽,甚至压根没配置端点——事件都还在。用 事件接口 按时间范围拉回来即可,不需要拿自己的订单表跟我们逐条比对。

商户后台的 Webhook 投递记录会展示每次尝试的 HTTP 状态;重试耗尽后,也可以在修复接收端后手动重发。

支付流程幂等与重试