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

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

© 2026 KukoPay. All rights reserved.

产品

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

开发者

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

公司

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

开始

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

对接指南

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

资金

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

API 参考

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

接口约定

列表、分页与导出metadata错误码
开发者文档
幂等与重试

幂等与重试

两层幂等机制,网络超时后安全重发请求,避免重复下单、退款或履约。

分布式系统里"没有收到响应"不等于"请求没有成功"。你发出下单请求后连接超时,可能是请求根本没到,也可能是订单已经创建、只是响应在回程中丢了——客户端无法区分这两种情况。

KukoPay 提供两层幂等,它们解决的问题不同,建议同时使用。

机制位置作用范围有效期
out_trade_no / out_refund_no请求体只覆盖下单和退款永久
Idempotency-Key请求头所有写接口24 小时

业务级:out_trade_no

out_trade_no 是商户订单号,同时也是当前商户、当前环境中的幂等键。同一个值重复提交时,接口返回首次创建的订单,不会重复创建或扣款。

{
  "out_trade_no": "ORDER_20260921_0001",
  "amount": 2990,
  "currency": "USD"
}

测试和正式环境相互隔离,因此两个环境可以分别使用相同的 out_trade_no。

正式环境的退款必须提供 out_refund_no 并以它作为幂等键。一次退款的所有重试都使用同一个值;另一笔退款必须换新值。

⚠️

out_trade_no 有一个盲区:同一个值配上不同的金额时,接口依然返回原订单。 代码里把金额算错这类 bug 会因此一直藏到对账时才暴露。下面的 Idempotency-Key 正是为了堵住它。

传输级:Idempotency-Key

在任何写接口上加一个 Idempotency-Key 请求头即可,值由你生成,推荐 UUID v4。平台会存下首次的响应并在 24 小时内原样重放,并且校验请求体是否一致。

curl -X POST "https://web.kukopay.com/api/v1/orders" \
  -H "X-Api-Key: kuko_test_你的密钥" \
  -H "Idempotency-Key: 8f14e45f-ea2a-4c6f-9d3b-1b7c2a55e901" \
  -H "Content-Type: application/json" \
  -d '{
    "out_trade_no": "ORDER_20260921_0001",
    "amount": 2990,
    "currency": "USD",
    "subject": "Pro 年度订阅"
  }'
规则说明
取值不超过 255 个可打印 ASCII 字符,推荐 UUID v4
作用范围按 商户 + 环境 + 接口 隔离,互不干扰
保留时长24 小时,之后该值可以被重新使用

命中重放时

平台原样返回首次的状态码和响应体,并额外带上 Idempotent-Replayed: true。注意 request_id 也是首次那一次的——这正是它有用的地方:你日志里的两条记录会指向同一次真实执行。

HTTP/1.1 201 Created
X-Request-Id: req_9f2c1a7b4e8d0356c1ab77de90f4b215
Idempotent-Replayed: true

同一个 Key 配不同的请求体

这是平台会明确拒绝的唯一情况,因为它一定意味着调用方出了 bug。

{
  "code": 400,
  "error": "idempotency_error",
  "type": "idempotency_error",
  "message": "该 Idempotency-Key 已被用于内容不同的请求。",
  "param": "Idempotency-Key",
  "request_id": "req_31b6c0d7f2a94e15b8c3d0a7e6f45219"
}
⚠️

重试时不要重新生成时间戳、随机数或签名字段。如果请求体必然会变,那就不要复用 Key——那本来就是一次新的请求。

并发使用同一个 Key

首次请求还在处理中时,第二次请求会立刻得到 409 idempotency_in_progress,而不是排队等待。稍后用同一个 Key 重试即可拿到最终结果。

哪些结果会被缓存

响应行为
2xx存下并在 24 小时内重放
4xx同样存下并重放——请求已有确定结论,重试不会有不同结果
5xx不缓存,Key 被释放。用同一个 Key 重试会真正执行

一个完整的重试实现

async function createOrder(payload) {
  // 每一笔业务订单生成一次,重试时复用同一个值
  const idempotencyKey = crypto.randomUUID()
  const delays = [1000, 2000, 4000, 8000]
 
  for (let attempt = 0; ; attempt++) {
    const res = await fetch("https://web.kukopay.com/api/v1/orders", {
      method: "POST",
      headers: {
        "X-Api-Key": process.env.KUKOPAY_API_KEY,
        "Idempotency-Key": idempotencyKey,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(payload),
    })
 
    const body = await res.json()
    // 把 request_id 记进日志,反馈问题时平台靠它定位
    console.info("kukopay", res.status, body.request_id)
 
    if (res.ok) return body.data
 
    // 409 表示同一个 Key 正在处理中;5xx 表示结果未知——都可以原样重试
    const retriable = res.status === 409 || res.status >= 500
    if (!retriable || attempt >= delays.length) throw new Error(body.message)
 
    await new Promise((r) => setTimeout(r, delays[attempt]))
  }
}

能否重试请看 错误码 页的 type 字段:api_error 与 api_connection_error 可以安全重试,invalid_request_error 修正参数前重试没有意义。

Webhook 消费

Webhook 可能因为超时或非 2xx 响应被重投。请在业务数据库为事件 id 建立唯一约束,并在同一事务内完成"写入事件 ID"和"业务履约"。

⚠️

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

异步通知 Webhook沙箱测试