Skip to content

幂等键

为避免网络重试导致重复开卡 / 重复扣款,写接口要求客户端在每次请求中提供 Idempotency-Key 头。

哪些接口需要

接口是否需要幂等键
/api/v1/openapi/card/apply✅ 必填
/api/v1/openapi/card/recharge✅ 必填
所有 /list 接口、/info 接口❌ 不需要

请求头

http
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
约束
长度128 字符
字符集URL-safe(推荐 UUID v4 / ULID)
去重窗口24 小时
缺失返回400 openapi_idempotency_key_required
超长返回400 openapi_idempotency_key_too_long
非法字符400 openapi_idempotency_key_invalid_chars
同 key 不同 body409 openapi_idempotency_key_conflict

服务端行为

同 key + 同 body

返回之前那次响应的副本(原状态码 + 原 data),即使第一次还在处理中也会等待并返回最终结果。不会触发第二次开卡。

回放的响应会附带一个额外的响应头,便于客户端区分是否是回放:

http
X-Idempotent-Replay: true

识别回放

X-Idempotent-Replay: true 仅用于提示,无论有没有这个头,响应体本身才是权威结果。该头主要用于打点和排错("这次请求其实已经到达服务端了,是网络抖动导致客户端重试")。

只有 2xx 响应会被缓存

服务端缓存原始调用返回 2xx 的响应体。若首次调用返回 4xx(如 insufficient_balance)或 5xx,相同 key + 相同 body 的下次请求会重新执行,不存在"回放"。

换句话说:幂等保护的是"成功响应丢失后的安全重试",不是"反复重试已经业务失败的请求"。

响应缓存上限

服务端最多缓存 256 KB 的响应体。当前所有 OpenAPI 响应都远低于此上限,正常使用不需关心 —— 仅作完整性披露。

同 key + 不同 body

返回 409 openapi_idempotency_key_conflict拒绝处理。客户端应换 key 重试或修正 body。

不同 key + 同 body

视为两次独立请求,会触发两次开卡 / 两次扣款。这是设计如此 —— 幂等基于 key,不是 body。

不要用 body hash 当 key

不要用"请求体 hash"作为 Idempotency-Key —— 这会让"用户连续两次请求同金额充值"被去重为一次。key 应代表一次业务意图,每次新业务请求生成新 key。

推荐用法

客户端 SDK 模式

python
import uuid

def apply_card(header_id, package_id):
    key = str(uuid.uuid4())  # 每次新业务一个新 key
    return call("/api/v1/openapi/card/apply",
                {"header_id": header_id, "package_id": package_id},
                idempotency_key=key)

异步任务模式

如果你的"开卡任务"会在网络失败后被任务队列重试,把 key 与任务记录绑定

python
def open_card_task(task_id, header_id, package_id):
    # 同一任务的所有重试用同一个 key
    key = f"openapi:apply:{task_id}"
    return call("/api/v1/openapi/card/apply", {...}, idempotency_key=key)

这样即使任务被重试 N 次,Coinepay 端也只会真正开卡一次。

与签名的关系

Idempotency-Key不参与签名输入构造(签名只覆盖 method/path/query/timestamp/nonce/body sha256)。但每次请求仍需新的 nonce,因为 nonce 是用来防签名重放的,与幂等是两套机制

机制防的是
Nonce攻击者抓到签名后重放请求
Idempotency-Key客户自己网络失败后安全重试

二者协同:客户重试同一笔业务时,用同一个 Idempotency-Key + 新的 Nonce + 新的 Timestamp + 新的 Signature

错误响应示例

缺失(400)

json
{
  "code": 400,
  "message": "缺少 Idempotency-Key 请求头",
  "message_key": "openapi_idempotency_key_required",
  "data": null
}

冲突(409)

json
{
  "code": 409,
  "message": "Idempotency-Key 冲突",
  "message_key": "openapi_idempotency_key_conflict",
  "data": null
}

采用 MIT 等价条款发布