跳到主要内容

幂等性

幂等性保证重发同一个请求不会产生第二笔付款。在分布式系统中,操作已经完成但响应丢失是常见情况——网络超时、中途部署——如果没有幂等性,重试就会变成重复扣款。

有两个字段控制这个行为:

字段位置作用
Idempotency-KeyHTTP 请求头标识这次尝试。我们用它找到已登记的付款并返回相同的结果。
identifier请求体字段标识你系统里的业务操作。我们会把它传给清算方,并在每个 webhook 中回传。

两者都是可选的:不传时我们会生成随机值。这一点很容易被忽略——不带这两个字段的请求永远被视为一笔全新的操作,完全没有防重复保护。如果你的重试需要安全,请同时传这两个字段,并且每次重试都使用完全相同的值。

Idempotency-Key 建议使用 UUID v4;identifier 建议使用你系统中已有的业务标识(批次号、付款单号)。

各业务的去重依据

去重依据按业务而异。用相同的值重发始终是安全的,区别在于你需要保持哪个字段不变。

业务去重依据失败后重发
PIX 出款与 PIX 退款identifier + Idempotency-Key重新执行
内部转账identifier + Idempotency-Key重新执行
Boleto 付款Idempotency-Key重新执行
TEDidentifier执行;返回上一次尝试的状态

场景

下面的例子以 PIX 出款为例,内部转账和 Boleto 付款同理。TED 的行为不同,单独说明。

操作成功后重发

返回 200 和原始结果——相同的 paymentIdendToEndId。不会产生新的扣款,第二次请求的请求体会被忽略。

{
"paymentId": "pay_9f2c…",
"status": "COMPLETED",
"endToEndId": "E1234567820260803…",
"identifier": "order-4471"
}

操作确定性失败后重发

包括账户余额不足、PIX 密钥无效、被反欺诈拒绝,以及被你自己配置的策略拦截。响应为 422,并带上原因:

{
"paymentId": "pay_9f2c…",
"status": "FAILED",
"errorCode": "insufficient_funds",
"errorReason": "PIX não autorizado por falta de saldo na conta"
}

确定性失败意味着资金没有出账,而且不会出账。用相同的 Idempotency-Key 重发会重新执行付款——在你补足余额或调整了拦截规则之后,这正是你需要的行为。paymentId 会被复用,因此你可以继续跟踪同一条记录,不必改跟一个新的。

只有在拒绝原因已经解决之后才重发。条件不变时重试只会得到相同的拒绝。

操作仍在处理中

在付款还没有最终结果时,用相同的 Idempotency-Key 重发会挂到正在执行的流程上,而不是开启第二个。响应为 202status"PENDING"。同一个 key 永远不会存在两笔并发付款。

结果不确定(TIMEOUT

这是 API 中最需要谨慎处理的情况。清算方接受了提交,但在等待窗口内没有确认结果:资金可能已经出账。我们返回 202status"TIMEOUT",并附带 warning

这里的行为是有意反过来的:用相同的 Idempotency-Key 重发会返回已有状态,不会再次提交付款,因为在结果未知的情况下重新执行是产生重复付款的典型路径。

处理方式:

  1. 调用 GET /v1/accounts/{accountId}/payments/{identifier} 查询,或等待 webhook——清算方确认后,迟到的最终结果(pix.out.completedpix.out.failed)会送达。
  2. 在发起新付款之前,先与账户流水核对。
  3. 只有在确认资金没有出账之后,才使用新的 Idempotency-Key

使用新的 Idempotency-Key 重发

这是一笔新的操作,会产生第二笔付款。更换 key 就是在明确表示"我要再付一次"。绝不要在自动重试循环中生成新的 key。

有一个有用的例外:如果你更换了 key 但沿用相同的 identifier,PIX 出款会识别出这是同一笔操作,返回 202 且不会创建第二笔付款。这只是额外的一道保护,不能替代 key——真正可靠的防重复保证来自重发时使用相同的 Idempotency-Key

用相同的 key 重发不同的请求体

我们不比较请求体:以 key 为准。如果该 Idempotency-Key 已有对应付款,我们会返回那笔付款的结果并忽略新的字段值——不会返回冲突错误。因此绝不要把同一个 key 用于不同的操作,否则你会收到 200,而它描述的并不是你刚刚请求的那笔付款。

TED

TED 只依据 identifier 去重,并且永不重新执行。任何使用已用过的 identifier 的重发都会返回 200 和该笔 TED 的当前状态,即使当前状态是失败:

{
"tedId": "ted-order-4471",
"status": "FAILED",
"identifier": "order-4471",
"note": "TED already exists for this identifier — returning current state (idempotent)"
}

TED 失败后若要重试,请使用新的 identifier。这是有意设计的:TED 的清算窗口很长(最多 48 小时),而 identifier 是把记录与清算方 webhook 关联起来的字段。

有效期

key 不会过期。付款记录是永久保存的,因此几个月前用过的 Idempotency-Key 仍然会返回那笔操作的结果。不要在不同的操作之间复用 key。

最佳实践

  • 在第一次尝试之前生成 Idempotency-Key,并与付款单一起持久化。在重试里生成的 key 起不到任何保护作用。
  • 针对网络错误和 5xx 的指数退避重试,全程使用同一个 key。
  • 始终传 identifier,使用该操作在你系统中已有的 id。它是 webhook 和工单排查中的追踪依据。
  • TIMEOUT 当作独立状态处理,绝不要当作失败。它是唯一一种重新发起就可能造成资金重复的结果。
  • 收到 422 后先解决原因再重发。errorCode 会告诉你是哪一种;完整清单见错误