幂等性
幂等性保证重发同一个请求不会产生第二笔付款。在分布式系统中,操作已经完成但响应丢失是常见情况——网络超时、中途部署——如果没有幂等性,重试就会变成重复扣款。
有两个字段控制这个行为:
| 字段 | 位置 | 作用 |
|---|---|---|
Idempotency-Key | HTTP 请求头 | 标识这次尝试。我们用它找到已登记的付款并返回相同的结果。 |
identifier | 请求体字段 | 标识你系统里的业务操作。我们会把它传给清算方,并在每个 webhook 中回传。 |
两者都是可选的:不传时我们会生成随机值。这一点很容易被忽略——不带这两个字段的请求永远被视为一笔全新的操作,完全没有防重复保护。如果你的重试需要安全,请同时传这两个字段,并且每次重试都使用完全相同的值。
Idempotency-Key 建议使用 UUID v4;identifier 建议使用你系统中已有的业务标识(批次号、付款单号)。
各业务的去重依据
去重依据按业务而异。用相同的值重发始终是安全的,区别在于你需要保持哪个字段不变。
| 业务 | 去重依据 | 失败后重发 |
|---|---|---|
| PIX 出款与 PIX 退款 | identifier + Idempotency-Key | 重新执行 |
| 内部转账 | identifier + Idempotency-Key | 重新执行 |
| Boleto 付款 | Idempotency-Key | 重新执行 |
| TED | identifier | 不执行;返回上一次尝试的状态 |
场景
下面的例子以 PIX 出款为例,内部转账和 Boleto 付款同理。TED 的行为不同,单独说明。
操作成功后重发
返回 200 和原始结果——相同的 paymentId 和 endToEndId。不会产生新的扣款,第二次请求的请求体会被忽略。
{
"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 重发会挂到正在执行的流程上,而不是开启第二个。响应为 202,status 为 "PENDING"。同一个 key 永远不会存在两笔并发付款。
结果不确定(TIMEOUT)
这是 API 中最需要谨慎处理的情况。清算方接受了提交,但在等待窗口内没有确认结果:资金可能已经出账。我们返回 202,status 为 "TIMEOUT",并附带 warning。
这里的行为是有意反过来的:用相同的 Idempotency-Key 重发会返回已有状态,不会再次提交付款,因为在结果未知的情况下重新执行是产生重复付款的典型路径。
处理方式:
- 调用
GET /v1/accounts/{accountId}/payments/{identifier}查询,或等待 webhook——清算方确认后,迟到的最终结果(pix.out.completed或pix.out.failed)会送达。 - 在发起新付款之前,先与账户流水核对。
- 只有在确认资金没有出账之后,才使用新的
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会告诉你是哪一种;完整清单见错误。