payment-idempotency
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseIdempotency & Duplicate Prevention
幂等性与重复请求防护
When this skill applies
本技能的适用场景
Use this skill when:
- Implementing any PPP endpoint handler that processes payments, cancellations, captures, or refunds
- Ensuring repeated Gateway calls with the same identifiers produce identical results without re-processing
- Building a payment state machine to prevent invalid transitions (e.g., capturing a cancelled payment)
- Handling the Gateway's 7-day retry window for status payments
undefined
Do not use this skill for:
- PPP endpoint response shapes and HTTP methods — use
payment-provider-protocol - Async callback URL notification logic — use
payment-async-flow - PCI compliance and Secure Proxy — use
payment-pci-security
在以下场景中使用本技能:
- 实现任何处理支付、取消、捕获或退款的PPP端点处理器
- 确保使用相同标识符的重复Gateway调用产生完全相同的结果,不会重复处理
- 构建支付状态机以防止无效转换(例如,捕获已取消的支付)
- 处理Gateway针对状态支付的7天重试窗口
undefined
请勿在以下场景使用本技能:
- PPP端点响应格式和HTTP方法 — 请使用
payment-provider-protocol - 异步回调URL通知逻辑 — 请使用
payment-async-flow - PCI合规与安全代理 — 请使用
payment-pci-security
Decision rules
决策规则
- Use as the idempotency key for Create Payment — every call with the same
paymentIdmust return the same result.paymentId - Use as the idempotency key for Cancel, Capture, and Refund operations.
requestId - If the Gateway sends a second Create Payment with the same , return the stored response without calling the acquirer again.
paymentId - Async payment methods (Boleto, Pix) MUST return — never
status: "undefined"until the acquirer confirms."approved" - A payment moves through defined states: →
undefined→approved, orsettled→undefined, ordenied→approved. Enforce valid transitions only.cancelled - Use a persistent data store (PostgreSQL, DynamoDB, VBase for VTEX IO) — never in-memory storage that is lost on restart.
- 在创建支付时,使用作为幂等键 — 所有使用相同
paymentId的调用必须返回相同结果。paymentId - 在取消、捕获和退款操作中,使用作为幂等键。
requestId - 如果Gateway发送第二个带有相同的创建支付请求,返回已存储的响应,无需再次调用收单机构(acquirer)。
paymentId - 异步支付方式(Boleto、Pix)必须返回— 直到收单机构确认后才能返回
status: "undefined"。"approved" - 支付需按照定义的状态流转:→
undefined→approved,或settled→undefined,或denied→approved。仅允许有效的状态转换。cancelled - 使用持久化数据存储(PostgreSQL、DynamoDB、VTEX IO的VBase) — 绝不能使用重启后会丢失的内存存储。
Hard constraints
硬性约束
Constraint: MUST use paymentId as idempotency key for Create Payment
约束:创建支付时必须使用paymentId作为幂等键
The connector MUST check for an existing record with the given before processing a new payment. If a record exists, return the stored response without calling the acquirer again.
paymentIdWhy this matters
The VTEX Gateway retries Create Payment requests with status for up to 7 days. Without idempotency on , each retry creates a new charge at the acquirer, resulting in duplicate charges to the customer. This is a financial loss and a critical production incident.
undefinedpaymentIdDetection
If the Create Payment handler does not check for an existing before processing, STOP. The handler must query the data store for the first.
paymentIdpaymentIdCorrect
typescript
async function createPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId } = req.body;
// Check for existing payment — idempotency guard
const existingPayment = await paymentStore.findByPaymentId(paymentId);
if (existingPayment) {
// Return the exact same response — no new acquirer call
res.status(200).json(existingPayment.response);
return;
}
// First time seeing this paymentId — process with acquirer
const result = await acquirer.authorize(req.body);
const response = {
paymentId,
status: result.status,
authorizationId: result.authorizationId ?? null,
nsu: result.nsu ?? null,
tid: result.tid ?? null,
acquirer: "MyProvider",
code: result.code ?? null,
message: result.message ?? null,
delayToAutoSettle: 21600,
delayToAutoSettleAfterAntifraud: 1800,
delayToCancel: 21600,
};
// Store the response for future idempotent lookups
await paymentStore.save(paymentId, { request: req.body, response });
res.status(200).json(response);
}Wrong
typescript
async function createPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId } = req.body;
// No idempotency check — every call hits the acquirer
// If the Gateway retries this (which it will for undefined status),
// the customer gets charged multiple times
const result = await acquirer.authorize(req.body);
res.status(200).json({
paymentId,
status: result.status,
authorizationId: result.authorizationId ?? null,
nsu: result.nsu ?? null,
tid: result.tid ?? null,
acquirer: "MyProvider",
code: null,
message: null,
delayToAutoSettle: 21600,
delayToAutoSettleAfterAntifraud: 1800,
delayToCancel: 21600,
});
}连接器在处理新支付前,必须检查是否存在给定的现有记录。如果存在记录,返回已存储的响应,无需再次调用收单机构。
paymentId重要性
VTEX Gateway会对状态的支付请求进行长达7天的重试。如果不对做幂等处理,每次重试都会向收单机构发起新的扣费请求,导致用户被重复扣费,这会造成财务损失并引发严重的生产事故。
undefinedpaymentId检测方式
如果创建支付处理器在处理前未检查现有,请立即停止。处理器必须先查询数据存储中的。
paymentIdpaymentId正确示例
typescript
async function createPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId } = req.body;
// 幂等性检查 — 防护重复请求
const existingPayment = await paymentStore.findByPaymentId(paymentId);
if (existingPayment) {
// 返回完全相同的响应 — 不调用收单机构
res.status(200).json(existingPayment.response);
return;
}
// 首次处理该paymentId — 调用收单机构处理
const result = await acquirer.authorize(req.body);
const response = {
paymentId,
status: result.status,
authorizationId: result.authorizationId ?? null,
nsu: result.nsu ?? null,
tid: result.tid ?? null,
acquirer: "MyProvider",
code: result.code ?? null,
message: result.message ?? null,
delayToAutoSettle: 21600,
delayToAutoSettleAfterAntifraud: 1800,
delayToCancel: 21600,
};
// 存储响应,用于后续幂等查询
await paymentStore.save(paymentId, { request: req.body, response });
res.status(200).json(response);
}错误示例
typescript
async function createPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId } = req.body;
// 无幂等性检查 — 每次调用都请求收单机构
// 如果Gateway重试(针对undefined状态一定会重试),用户会被多次扣费
const result = await acquirer.authorize(req.body);
res.status(200).json({
paymentId,
status: result.status,
authorizationId: result.authorizationId ?? null,
nsu: result.nsu ?? null,
tid: result.tid ?? null,
acquirer: "MyProvider",
code: null,
message: null,
delayToAutoSettle: 21600,
delayToAutoSettleAfterAntifraud: 1800,
delayToCancel: 21600,
});
}Constraint: MUST return identical response for duplicate requests
约束:重复请求必须返回完全相同的响应
When the connector receives a Create Payment request with a that already exists in the data store, it MUST return the exact stored response. It MUST NOT create a new record, generate new identifiers, or re-process the payment.
paymentIdWhy this matters
The Gateway uses the response fields (, , , ) to track the transaction. If a retry returns different values, the Gateway loses track of the original transaction, causing reconciliation failures and potential double settlements.
authorizationIdtidnsustatusDetection
If the handler creates a new database record or generates new identifiers when it finds an existing , STOP. The handler must return the previously stored response verbatim.
paymentIdCorrect
typescript
async function createPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId } = req.body;
const existing = await paymentStore.findByPaymentId(paymentId);
if (existing) {
// Return the EXACT stored response — same authorizationId, tid, nsu, status
res.status(200).json(existing.response);
return;
}
// ... process new payment and store response
}Wrong
typescript
async function createPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId } = req.body;
const existing = await paymentStore.findByPaymentId(paymentId);
if (existing) {
// WRONG: Generating new identifiers for an existing payment
// The Gateway will see different tid/nsu and lose track of the transaction
const newTid = generateNewTid();
res.status(200).json({
...existing.response,
tid: newTid, // Different from original — breaks reconciliation
nsu: generateNewNsu(),
});
return;
}
// ... process new payment
}当连接器收到带有已存在于数据存储中的的创建支付请求时,必须返回完全相同的存储响应。不得创建新记录、生成新标识符或重新处理该支付。
paymentId重要性
Gateway使用响应字段(、、、)来跟踪交易。如果重试返回不同的值,Gateway会丢失对原交易的跟踪,导致对账失败和潜在的重复结算。
authorizationIdtidnsustatus检测方式
如果处理器在发现现有时创建新的数据库记录或生成新标识符,请立即停止。处理器必须原封不动地返回之前存储的响应。
paymentId正确示例
typescript
async function createPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId } = req.body;
const existing = await paymentStore.findByPaymentId(paymentId);
if (existing) {
// 返回完全相同的存储响应 — 相同的authorizationId、tid、nsu、status
res.status(200).json(existing.response);
return;
}
// ... 处理新支付并存储响应
}错误示例
typescript
async function createPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId } = req.body;
const existing = await paymentStore.findByPaymentId(paymentId);
if (existing) {
// 错误:为已有支付生成新标识符
// Gateway会看到不同的tid/nsu,从而丢失交易跟踪
const newTid = generateNewTid();
res.status(200).json({
...existing.response,
tid: newTid, // 与原响应不同 — 破坏对账
nsu: generateNewNsu(),
});
return;
}
// ... 处理新支付
}Constraint: MUST NOT approve async payments synchronously
约束:不得同步批准异步支付
If a payment method is asynchronous (e.g., Boleto, Pix, bank redirect), the Create Payment response MUST return . It MUST NOT return or until the payment is actually confirmed or rejected by the acquirer.
status: "undefined"status: "approved"status: "denied"Why this matters
Returning for an async method tells the Gateway the payment is confirmed before the customer has actually paid. The order ships, but no money was collected. The merchant loses the product and the revenue. The correct flow is to return and use the to notify the Gateway when the payment is confirmed.
approvedundefinedcallbackUrlDetection
If the Create Payment handler returns or for an asynchronous payment method (Boleto, Pix, bank transfer, redirect-based), STOP. Async methods must return and use callbacks.
status: "approved"status: "denied""undefined"Correct
typescript
async function createPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId, paymentMethod, callbackUrl } = req.body;
const isAsyncMethod = ["BankInvoice", "Pix"].includes(paymentMethod);
if (isAsyncMethod) {
const pending = await acquirer.initiateAsyncPayment(req.body);
await paymentStore.save(paymentId, {
status: "undefined",
callbackUrl,
acquirerRef: pending.reference,
});
res.status(200).json({
paymentId,
status: "undefined", // Correct for async
authorizationId: pending.authorizationId ?? null,
nsu: pending.nsu ?? null,
tid: pending.tid ?? null,
acquirer: "MyProvider",
code: "ASYNC-PENDING",
message: "Awaiting customer payment",
delayToAutoSettle: 21600,
delayToAutoSettleAfterAntifraud: 1800,
delayToCancel: 604800, // 7 days for async
paymentUrl: pending.paymentUrl,
});
return;
}
// Sync methods can return approved/denied immediately
// ...
}Wrong
typescript
async function createPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId, paymentMethod } = req.body;
// WRONG: Approving a Pix payment synchronously
// The customer hasn't paid yet — the order will ship without payment
const result = await acquirer.createPixCharge(req.body);
res.status(200).json({
paymentId,
status: "approved", // WRONG — Pix is async, should be "undefined"
authorizationId: result.authorizationId ?? null,
nsu: null,
tid: null,
acquirer: "MyProvider",
code: null,
message: null,
delayToAutoSettle: 21600,
delayToAutoSettleAfterAntifraud: 1800,
delayToCancel: 21600,
});
}如果支付方式是异步的(例如Boleto、Pix、银行跳转),创建支付的响应必须返回。在收单机构实际确认或拒绝支付前,不得返回或。
status: "undefined"status: "approved"status: "denied"重要性
为异步支付方式返回会让Gateway认为支付已确认,但实际上用户还未完成支付。订单会被发货,但未收到款项,商家会同时损失商品和收入。正确的流程是返回,并在支付确认时使用通知Gateway。
approvedundefinedcallbackUrl检测方式
如果创建支付处理器为异步支付方式(Boleto、Pix、银行转账、跳转类)返回或,请立即停止。异步方式必须返回并使用回调。
status: "approved"status: "denied""undefined"正确示例
typescript
async function createPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId, paymentMethod, callbackUrl } = req.body;
const isAsyncMethod = ["BankInvoice", "Pix"].includes(paymentMethod);
if (isAsyncMethod) {
const pending = await acquirer.initiateAsyncPayment(req.body);
await paymentStore.save(paymentId, {
status: "undefined",
callbackUrl,
acquirerRef: pending.reference,
});
res.status(200).json({
paymentId,
status: "undefined", // 异步支付的正确状态
authorizationId: pending.authorizationId ?? null,
nsu: pending.nsu ?? null,
tid: pending.tid ?? null,
acquirer: "MyProvider",
code: "ASYNC-PENDING",
message: "等待用户支付",
delayToAutoSettle: 21600,
delayToAutoSettleAfterAntifraud: 1800,
delayToCancel: 604800, // 异步支付的取消延迟为7天
paymentUrl: pending.paymentUrl,
});
return;
}
// 同步支付方式可立即返回approved/denied
// ...
}错误示例
typescript
async function createPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId, paymentMethod } = req.body;
// 错误:同步批准Pix支付
// 用户尚未支付,但订单会被发货
const result = await acquirer.createPixCharge(req.body);
res.status(200).json({
paymentId,
status: "approved", // 错误 — Pix是异步方式,应返回"undefined"
authorizationId: result.authorizationId ?? null,
nsu: null,
tid: null,
acquirer: "MyProvider",
code: null,
message: null,
delayToAutoSettle: 21600,
delayToAutoSettleAfterAntifraud: 1800,
delayToCancel: 21600,
});
}Preferred pattern
推荐模式
Payment state store with idempotency support:
typescript
interface PaymentRecord {
paymentId: string;
status: "undefined" | "approved" | "denied" | "cancelled" | "settled" | "refunded";
response: Record<string, unknown>;
callbackUrl?: string;
createdAt: Date;
updatedAt: Date;
}
interface OperationRecord {
requestId: string;
paymentId: string;
operation: "cancel" | "capture" | "refund";
response: Record<string, unknown>;
createdAt: Date;
}Idempotent Create Payment with state machine:
typescript
const store = new PaymentStore();
async function createPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId, paymentMethod, callbackUrl } = req.body;
// Idempotency check
const existing = await store.findByPaymentId(paymentId);
if (existing) {
res.status(200).json(existing.response);
return;
}
const isAsync = ["BankInvoice", "Pix"].includes(paymentMethod);
const result = await acquirer.process(req.body);
const status = isAsync ? "undefined" : result.status;
const response = {
paymentId,
status,
authorizationId: result.authorizationId ?? null,
nsu: result.nsu ?? null,
tid: result.tid ?? null,
acquirer: "MyProvider",
code: result.code ?? null,
message: result.message ?? null,
delayToAutoSettle: 21600,
delayToAutoSettleAfterAntifraud: 1800,
delayToCancel: isAsync ? 604800 : 21600,
...(result.paymentUrl ? { paymentUrl: result.paymentUrl } : {}),
};
await store.save(paymentId, {
paymentId, status, response, callbackUrl,
createdAt: new Date(), updatedAt: new Date(),
});
res.status(200).json(response);
}Idempotent Cancel with guard and state validation:
requestIdtypescript
async function cancelPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId } = req.params;
const { requestId } = req.body;
// Operation idempotency check
const existingOp = await store.findOperation(requestId);
if (existingOp) {
res.status(200).json(existingOp.response);
return;
}
// State machine validation
const payment = await store.findByPaymentId(paymentId);
if (!payment || !["undefined", "approved"].includes(payment.status)) {
res.status(200).json({
paymentId,
cancellationId: null,
code: "cancel-failed",
message: `Cannot cancel payment in ${payment?.status ?? "unknown"} state`,
requestId,
});
return;
}
const result = await acquirer.cancel(paymentId);
const response = {
paymentId,
cancellationId: result.cancellationId ?? null,
code: result.code ?? null,
message: result.message ?? "Successfully cancelled",
requestId,
};
await store.updateStatus(paymentId, "cancelled");
await store.saveOperation(requestId, {
requestId, paymentId, operation: "cancel", response, createdAt: new Date(),
});
res.status(200).json(response);
}支持幂等性的支付状态存储:
typescript
interface PaymentRecord {
paymentId: string;
status: "undefined" | "approved" | "denied" | "cancelled" | "settled" | "refunded";
response: Record<string, unknown>;
callbackUrl?: string;
createdAt: Date;
updatedAt: Date;
}
interface OperationRecord {
requestId: string;
paymentId: string;
operation: "cancel" | "capture" | "refund";
response: Record<string, unknown>;
createdAt: Date;
}带有状态机的幂等性创建支付:
typescript
const store = new PaymentStore();
async function createPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId, paymentMethod, callbackUrl } = req.body;
// 幂等性检查
const existing = await store.findByPaymentId(paymentId);
if (existing) {
res.status(200).json(existing.response);
return;
}
const isAsync = ["BankInvoice", "Pix"].includes(paymentMethod);
const result = await acquirer.process(req.body);
const status = isAsync ? "undefined" : result.status;
const response = {
paymentId,
status,
authorizationId: result.authorizationId ?? null,
nsu: result.nsu ?? null,
tid: result.tid ?? null,
acquirer: "MyProvider",
code: result.code ?? null,
message: result.message ?? null,
delayToAutoSettle: 21600,
delayToAutoSettleAfterAntifraud: 1800,
delayToCancel: isAsync ? 604800 : 21600,
...(result.paymentUrl ? { paymentUrl: result.paymentUrl } : {}),
};
await store.save(paymentId, {
paymentId, status, response, callbackUrl,
createdAt: new Date(), updatedAt: new Date(),
});
res.status(200).json(response);
}基于RequestId防护和状态验证的幂等性取消:
typescript
async function cancelPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId } = req.params;
const { requestId } = req.body;
// 操作幂等性检查
const existingOp = await store.findOperation(requestId);
if (existingOp) {
res.status(200).json(existingOp.response);
return;
}
// 状态机验证
const payment = await store.findByPaymentId(paymentId);
if (!payment || !["undefined", "approved"].includes(payment.status)) {
res.status(200).json({
paymentId,
cancellationId: null,
code: "cancel-failed",
message: `无法取消处于${payment?.status ?? "未知"}状态的支付`,
requestId,
});
return;
}
const result = await acquirer.cancel(paymentId);
const response = {
paymentId,
cancellationId: result.cancellationId ?? null,
code: result.code ?? null,
message: result.message ?? "取消成功",
requestId,
};
await store.updateStatus(paymentId, "cancelled");
await store.saveOperation(requestId, {
requestId, paymentId, operation: "cancel", response, createdAt: new Date(),
});
res.status(200).json(response);
}Common failure modes
常见失败模式
- Processing duplicate payments — Calling the acquirer for every Create Payment request without checking if the already exists. The Gateway retries
paymentIdpayments for up to 7 days, so a single $100 payment can result in hundreds of duplicate charges.undefined - Synchronous approval of async payment methods — Returning immediately for Boleto or Pix before the customer has actually paid. The order ships without payment collected.
status: "approved" - Losing state between retries — Storing payment state in memory (, local variable) instead of a persistent database. On process restart, all state is lost and the next retry creates a duplicate charge.
Map - Generating new identifiers for duplicate requests — Returning different ,
tid, ornsuvalues when the Gateway retries with the sameauthorizationId. This breaks Gateway reconciliation and can cause double settlements.paymentId - Ignoring requestId on Cancel/Capture/Refund — Not checking before processing operations, causing duplicate cancellations or refunds when the Gateway retries.
requestId
- 重复处理支付 — 每次创建支付请求都调用收单机构,未检查是否已存在。Gateway会对
paymentId状态的支付重试长达7天,一笔100美元的支付可能导致数百次重复扣费。undefined - 同步批准异步支付 — 对于Boleto或Pix等异步支付方式,立即返回,而用户尚未完成支付。订单会被发货,但未收到款项。
status: "approved" - 重试间丢失状态 — 使用内存存储(、局部变量)存储支付状态,而非持久化数据库。重启服务后,所有状态丢失,下一次重试会创建重复扣费。
Map - 为重复请求生成新标识符 — 当Gateway使用相同重试时,返回不同的
paymentId、tid或nsu值。这会破坏Gateway的对账,导致重复结算。authorizationId - 取消/捕获/退款时忽略requestId — 处理操作前未检查,导致Gateway重试时出现重复取消或退款。
requestId
Review checklist
审核清单
- Does the Create Payment handler check the data store for an existing before calling the acquirer?
paymentId - Are stored responses returned verbatim for duplicate requests?
paymentId - Do Cancel, Capture, and Refund handlers check for existing before processing?
requestId - Is the payment state machine enforced (e.g., cannot capture a cancelled payment)?
- Do async payment methods (Boleto, Pix) return instead of
status: "undefined"?"approved" - Is payment state stored in a persistent database (not in-memory)?
- Are values extended for async methods (e.g., 604800 seconds = 7 days)?
delayToCancel
- 创建支付处理器在调用收单机构前,是否检查数据存储中是否存在现有?
paymentId - 对于重复的请求,是否原封不动地返回存储的响应?
paymentId - 取消、捕获和退款处理器在处理前是否检查现有?
requestId - 是否强制执行支付状态机(例如,无法捕获已取消的支付)?
- 异步支付方式(Boleto、Pix)是否返回而非
status: "undefined"?"approved" - 支付状态是否存储在持久化数据库中(而非内存)?
- 异步支付的值是否已延长(例如604800秒=7天)?
delayToCancel
Related skills
相关技能
- — Endpoint contracts and response shapes
payment-provider-protocol - — Callback URL notification and the 7-day retry window
payment-async-flow - — PCI compliance and Secure Proxy
payment-pci-security
- — 端点契约和响应格式
payment-provider-protocol - — 回调URL通知和7天重试窗口
payment-async-flow - — PCI合规与安全代理
payment-pci-security
Reference
参考资料
- Implementing a Payment Provider — Official guide explaining idempotency requirements for Cancel, Capture, and Refund operations
- Developing a Payment Connector for VTEX — Help Center guide with idempotency implementation steps using paymentId and VBase
- Purchase Flows — Detailed authorization, capture, and cancellation flow documentation including retry behavior
- Payment Provider Protocol (Help Center) — Protocol overview including callback URL retry mechanics and 7-day retry window
- Payment Provider Protocol API Reference — Full API specification with requestId and paymentId field definitions
- Implementing a Payment Provider — 官方指南,解释取消、捕获和退款操作的幂等性要求
- Developing a Payment Connector for VTEX — 帮助中心指南,包含使用paymentId和VBase实现幂等性的步骤
- Purchase Flows — 详细的授权、捕获和取消流程文档,包括重试行为
- Payment Provider Protocol (Help Center) — 协议概述,包括回调URL重试机制和7天重试窗口
- Payment Provider Protocol API Reference — 完整的API规范,包含requestId和paymentId字段定义