Webhook 的补偿通道——账户上发生的每一件事都可以在这里重新读回。
GET /api/v1/events
GET /api/v1/events/{event_id}事件是账户上发生的事实,Webhook 只是把它告诉你的一次尝试。两者分开的好处很实际:接收端宕机一整天、重试全部耗尽,甚至压根没配置过端点,事件都还在。
除 通用列表参数 外:
| 参数 | 说明 |
|---|---|
type | 事件类型,见 Webhook |
reference_id | 事件主体的单号:订单号 / 退款号 / 拒付号 |
# 拉回昨天所有支付成功事件
curl -G "https://web.kukopay.com/api/v1/events" \
-H "X-Api-Key: kuko_live_你的密钥" \
-d type=payment.succeeded \
-d "created[gte]=2026-09-20T00:00:00Z" \
-d "created[lte]=2026-09-20T23:59:59Z" \
-d limit=100收到 Webhook 但当时没能处理完时,用事件 id 从源头重新取一次,而不是信任本地缓存的报文。
curl "https://web.kukopay.com/api/v1/events/evt_3f2a9c7e1b4d0568a2cf" \
-H "X-Api-Key: kuko_live_你的密钥"{
"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,
"status": "paid"
}
}
}data.object 与对应查询接口返回的对象完全一致,也与 Webhook 报文里的一致——一套解析代码通用。
事件 id 由事件类型和主体单号推导而来,是确定性的。同一笔订单的 payment.succeeded 无论被多少条路径触发,都只会产生一个事件,所以按 id 去重是可靠的。
// 接收端恢复后,把宕机窗口内的事件补回来
async function backfill(since, until) {
let startingAfter
while (true) {
const query = new URLSearchParams({
limit: "100",
"created[gte]": since,
"created[lte]": until,
})
if (startingAfter) query.set("starting_after", startingAfter)
const res = await fetch(
`https://web.kukopay.com/api/v1/events?${query}`,
{ headers: { "X-Api-Key": process.env.KUKOPAY_API_KEY } }
)
const { data: page } = await res.json()
for (const event of page.data) {
// 与 Webhook 走同一个处理函数,按 event.id 去重
await handleEvent(event)
}
if (!page.has_more) return
startingAfter = page.data[page.data.length - 1].id
}
}事件按创建时间倒序返回,所以补数据时用 created[gte] / created[lte] 圈定窗口,比依赖游标从头翻更省事。