关于 payment-provider-protocol
payment-provider-protocol 是帮助开发者构建 VTEX 支付连接器中间件的技能,涵盖 PPP 端点实现与测试套件认证要求。它整理了必需的支付流程端点、响应字段及清单配置规则。
主要功能
- 01实现六个必需的支付流程端点
- 02遵循 PPP 响应字段与状态码要求
- 03在清单中声明支持的支付方式
- 04提供可选的商户配置流程端点
- 05核对传输安全与响应时限要求
适用场景
- 01从头构建 PSP 与 VTEX 的连接器
- 02调试或扩展 PPP 端点
- 03准备 VTEX Payment Provider Test Suite 认证
- 04检查支付端点的响应结构是否完整
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ vtex/ skills --skill payment-provider-protocol安装后在 Claude Code 中输入/payment-provider-protocol确认已加载。
详细介绍
PPP 端点实现
适用场景
在以下场景中使用此技能:
- 构建新的支付连接器中间件,将 PSP 集成到 VTEX Payment Gateway
- 实现、调试或扩展 9 个 PPP 端点中的任意端点
- 准备连接器,以通过 VTEX Payment Provider Test Suite 认证
以下场景不适用此技能:
- 幂等性和重复请求预防逻辑——请使用
payment-idempotency - 异步支付流程和回调 URL——请使用
payment-async-flow - PCI 合规性和 Secure Proxy 卡片处理——请使用
payment-pci-security - POS / VTEX Sales App 的持卡支付(
Venda Direta Credito、Venda Direta Debito、终端 Payment Apps)——请结合此技能使用payment-ppp-pos,以了解端点结构
决策规则
- 连接器必须实现全部 6 个支付流程端点:Manifest、Create Payment、Cancel、Capture/Settle、Refund、Inbound Request。
- 配置流程(3 个端点:Create Auth Token、Provider Auth Redirect、Get Credentials)为可选,但建议用于商户入驻。
- 所有端点都必须通过 HTTPS 在 443 端口上提供服务,并使用 TLS 1.2。
- 在认证测试期间,连接器必须在 5 秒内响应;在生产环境中,必须在 20 秒内响应。
- 支付服务提供商必须通过 PCI-DSS 认证,或在处理卡片支付时使用 Secure Proxy。
- 所有调用均由 Gateway 发起。除通过
callbackUrl(异步通知)和 Secure Proxy(转发卡片数据)外,中间件不得调用 Gateway。
硬性约束
约束:实现所有必需的支付流程端点
连接器必须实现全部六个支付流程端点:GET /manifest、POST /payments、POST /payments/{paymentId}/cancellations、POST /payments/{paymentId}/settlements、POST /payments/{paymentId}/refunds,以及 POST /payments/{paymentId}/inbound-request/{action}。
为什么这很重要 VTEX Payment Provider Test Suite 会在认证期间验证每个端点。缺少端点会导致测试失败,连接器也无法通过审核。在运行时,Gateway 需要所有端点——缺少取消端点意味着无法撤销支付。
检测 如果连接器路由器/处理程序文件未定义全部 6 个支付流程路径的处理程序,请停止并添加缺失的端点,然后再继续。
正确
import { Router } from "express";
const router = Router();
// All 6 payment-flow endpoints implemented
router.get("/manifest", manifestHandler);
router.post("/payments", createPaymentHandler);
router.post("/payments/:paymentId/cancellations", cancelPaymentHandler);
router.post("/payments/:paymentId/settlements", capturePaymentHandler);
router.post("/payments/:paymentId/refunds", refundPaymentHandler);
router.post("/payments/:paymentId/inbound-request/:action", inboundRequestHandler);
export default router;
错误
import { Router } from "express";
const router = Router();
// Missing manifest, inbound-request, and refund endpoints
// This will fail homologation and break runtime operations
router.post("/payments", createPaymentHandler);
router.post("/payments/:paymentId/cancellations", cancelPaymentHandler);
router.post("/payments/:paymentId/settlements", capturePaymentHandler);
export default router;
约束:返回正确的 HTTP 状态码和响应结构
每个端点都必须返回 PPP API 文档规定的确切响应结构。Create Payment 必须返回 paymentId、status、authorizationId、tid、nsu、acquirer、code、message、delayToAutoSettle、delayToAutoSettleAfterAntifraud 和 delayToCancel。Cancel 必须返回 paymentId、cancellationId、code、message、requestId。Capture 必须返回 paymentId、settleId、value、code、message、requestId。Refund 必须返回 paymentId、refundId、value、code、message、requestId。
为什么这很重要
Gateway 会以编程方式解析这些字段。缺少字段会导致反序列化错误,Gateway 会将支付视为失败。delayToAutoSettle 值不正确会导致支付在错误的时间自动取消或自动扣款。
检测 如果响应对象缺少其端点所需的任何字段,请停止并添加缺失字段。
正确
interface CreatePaymentResponse {
paymentId: string;
status: "approved" | "denied" | "undefined";
authorizationId: string | null;
nsu: string | null;
tid: string | null;
acquirer: string | null;
code: string | null;
message: string | null;
delayToAutoSettle: number;
delayToAutoSettleAfterAntifraud: number;
delayToCancel: number;
paymentUrl?: string;
}
async function createPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId, value, currency, paymentMethod, card, callbackUrl } = req.body;
const result = await processPaymentWithAcquirer(req.body);
const response: CreatePaymentResponse = {
paymentId,
status: result.status,
authorizationId: result.authorizationId ?? null,
nsu: result.nsu ?? null,
tid: result.tid ?? null,
acquirer: "MyAcquirer",
code: result.code ?? null,
message: result.message ?? null,
delayToAutoSettle: 21600, // 6 hours in seconds
delayToAutoSettleAfterAntifraud: 1800, // 30 minutes in seconds
delayToCancel: 21600, // 6 hours in seconds
};
res.status(200).json(response);
}
错误
// Missing required fields — Gateway will reject this response
async function createPaymentHandler(req: Request, res: Response): Promise<void> {
const result = await processPaymentWithAcquirer(req.body);
// Missing: authorizationId, nsu, tid, acquirer, code, message,
// delayToAutoSettle, delayToAutoSettleAfterAntifraud, delayToCancel
res.status(200).json({
paymentId: req.body.paymentId,
status: result.status,
});
}
约束:清单必须声明所有支持的支付方式
GET /manifest 端点必须返回一个 paymentMethods 数组,其中列出连接器支持的所有支付方式,并为每种方式配置正确的 name 和 allowsSplit。
为何重要
Gateway 会读取清单,以确定有哪些可用的支付方式。如果缺少某种方式,商家就无法在 VTEX Admin 中配置该方式。allowsSplit 值错误会导致拆分支付失败。
检测
如果清单处理程序返回空的 paymentMethods 数组,或将服务提供方实际不支持的方式硬编码在其中,请立即停止并修复清单。
正确
async function manifestHandler(_req: Request, res: Response): Promise<void> {
const manifest = {
paymentMethods: [
{ name: "Visa", allowsSplit: "onCapture" },
{ name: "Mastercard", allowsSplit: "onCapture" },
{ name: "American Express", allowsSplit: "onCapture" },
{ name: "BankInvoice", allowsSplit: "onAuthorize" },
{ name: "Pix", allowsSplit: "disabled" },
],
};
res.status(200).json(manifest);
}
错误
// Empty manifest — no payment methods will appear in the Admin
async function manifestHandler(_req: Request, res: Response): Promise<void> {
res.status(200).json({ paymentMethods: [] });
}
推荐模式
架构概览:
Shopper → VTEX Checkout → VTEX Payment Gateway → [Your Connector Middleware] → Acquirer/PSP
↕
Configuration Flow (Admin)
适用于所有端点契约的 TypeScript 接口:
// --- Manifest ---
interface ManifestResponse {
paymentMethods: Array<{
name: string;
allowsSplit: "onCapture" | "onAuthorize" | "disabled";
}>;
}
// --- Create Payment ---
interface CreatePaymentRequest {
reference: string;
orderId: string;
transactionId: string;
paymentId: string;
paymentMethod: string;
value: number;
currency: string;
installments: number;
card?: {
holder: string;
number: string;
csc: string;
expiration: { month: string; year: string };
};
miniCart: Record<string, unknown>;
callbackUrl: string;
returnUrl?: string;
}
interface CreatePaymentResponse {
paymentId: string;
status: "approved" | "denied" | "undefined";
authorizationId: string | null;
nsu: string | null;
tid: string | null;
acquirer: string | null;
code: string | null;
message: string | null;
delayToAutoSettle: number;
delayToAutoSettleAfterAntifraud: number;
delayToCancel: number;
paymentUrl?: string;
}
// --- Cancel Payment ---
interface CancelPaymentResponse {
paymentId: string;
cancellationId: string | null;
code: string | null;
message: string | null;
requestId: string;
}
// --- Capture/Settle Payment ---
interface CapturePaymentResponse {
paymentId: string;
settleId: string | null;
value: number;
code: string | null;
message: string | null;
requestId: string;
}
// --- Refund Payment ---
interface RefundPaymentResponse {
paymentId: string;
refundId: string | null;
value: number;
code: string | null;
message: string | null;
requestId: string;
}
// --- Inbound Request ---
interface InboundResponse {
requestId: string;
paymentId: string;
responseData: {
statusCode: number;
contentType: string;
content: string;
};
}
包含全部 6 个端点的完整支付流程路由器:
import { Router, Request, Response } from "express";
const router = Router();
router.get("/manifest", async (_req: Request, res: Response) => {
res.status(200).json({
paymentMethods: [
{ name: "Visa", allowsSplit: "onCapture" },
{ name: "Mastercard", allowsSplit: "onCapture" },
{ name: "Pix", allowsSplit: "disabled" },
],
});
});
router.post("/payments", async (req: Request, res: Response) => {
const body: CreatePaymentRequest = req.body;
const result = await processWithAcquirer(body);
const response: CreatePaymentResponse = {
paymentId: body.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,
};
res.status(200).json(response);
});
router.post("/payments/:paymentId/cancellations", async (req: Request, res: Response) => {
const { paymentId } = req.params;
const { requestId } = req.body;
const result = await cancelWithAcquirer(paymentId);
res.status(200).json({
paymentId,
cancellationId: result.cancellationId ?? null,
code: result.code ?? null,
message: result.message ?? "Successfully cancelled",
requestId,
});
});
router.post("/payments/:paymentId/settlements", async (req: Request, res: Response) => {
const body = req.body;
const result = await captureWithAcquirer(body.paymentId, body.value);
res.status(200).json({
paymentId: body.paymentId,
settleId: result.settleId ?? null,
value: result.capturedValue ?? body.value,
code: result.code ?? null,
message: result.message ?? null,
requestId: body.requestId,
});
});
router.post("/payments/:paymentId/refunds", async (req: Request, res: Response) => {
const body = req.body;
const result = await refundWithAcquirer(body.paymentId, body.value);
res.status(200).json({
paymentId: body.paymentId,
refundId: result.refundId ?? null,
value: result.refundedValue ?? body.value,
code: result.code ?? null,
message: result.message ?? null,
requestId: body.requestId,
});
});
router.post("/payments/:paymentId/inbound-request/:action", async (req: Request, res: Response) => {
const body = req.body;
const result = await handleInbound(body);
res.status(200).json({
requestId: body.requestId,
paymentId: body.paymentId,
responseData: {
statusCode: 200,
contentType: "application/json",
content: JSON.stringify(result),
},
});
});
export default router;
配置流程端点(可选,用于商家入驻):
import { Router, Request, Response } from "express";
const configRouter = Router();
// 1. POST /authorization/token
configRouter.post("/authorization/token", async (req: Request, res: Response) => {
const { applicationId, returnUrl } = req.body;
const token = await generateAuthorizationToken(applicationId, returnUrl);
res.status(200).json({ applicationId, token });
});
// 2. GET /authorization/redirect
configRouter.get("/authorization/redirect", async (req: Request, res: Response) => {
const { token } = req.query;
const providerLoginUrl = buildProviderLoginUrl(token as string);
res.redirect(302, providerLoginUrl);
});
// 3. GET /authorization/credentials
configRouter.get("/authorization/credentials", async (req: Request, res: Response) => {
const { authorizationCode } = req.query;
const credentials = await exchangeCodeForCredentials(authorizationCode as string);
res.status(200).json({
applicationId: "vtex",
appKey: credentials.appKey,
appToken: credentials.appToken,
});
});
export default configRouter;
常见故障模式
- 端点实现不完整 — 只实现创建支付和捕获,跳过清单、取消、退款和入站请求。测试套件会测试所有端点,因此验证测试会失败。运行时,Gateway 无法取消或退款。
- HTTP 方法错误 — 对清单端点使用 POST,或对创建支付使用 GET。Gateway 会发送特定的 HTTP 方法;处理程序不匹配时会返回 404 或 405。
- 缺少延迟值或延迟值为零 — 在创建支付响应中省略
delayToAutoSettle、delayToAutoSettleAfterAntifraud或delayToCancel,或将其设为零。这会导致立即自动捕获或自动取消,造成提前结算或支付丢失。 - 响应结构不完整 — 只返回
paymentId和status,却未提供authorizationId、tid、nsu、acquirer等字段。Gateway 会反序列化所有字段,并将缺失字段视为失败。
审查清单
- 是否已实现全部 6 个支付流程端点(清单、创建支付、取消、捕获、退款、入站请求)?
- 每个端点是否都返回包含所有必需字段的完整响应结构?
- 清单是否声明了服务提供方实际支持的所有支付方式?
- 是否使用了正确的 HTTP 方法(清单使用 GET,其余端点使用 POST)?
-
delayToAutoSettle、delayToAutoSettleAfterAntifraud和delayToCancel是否设为合理的非零值? - 连接器是否通过 HTTPS 在端口 443 上提供服务,并使用 TLS 1.2?
- 连接器是否能在测试套件中于 5 秒内响应,在生产环境中于 20 秒内响应?
- 如果需要商家自行入驻,是否已实现配置流程端点?
相关技能
payment-idempotency— 幂等键(paymentId、requestId)和用于防止重复操作的状态机payment-async-flow— 异步支付方式、callbackUrl和 7 天重试窗口payment-pci-security— PCI 合规性、Secure Proxy 和银行卡数据处理payment-ppp-pos— POS / Sales App 面对面刷卡覆盖层;将其与本技能搭配使用,了解端点结构vtex-io-application-performance— 按客户端调整超时和重试、VBase 正确性约束,以及基于 IO 的支付连接器的结构化日志
参考资料
- 支付服务商协议概览 — API 概览,包含端点要求、常用参数和测试套件信息
- 实现支付服务商 — 分步指南,涵盖全部 9 个端点及请求/响应示例
- 支付服务商协议(帮助中心) — 协议高级说明,包含支付流程图和回调 URL 用法
- 购买流程 — 授权、捕获和取消流程详情
- 支付服务商协议 API 参考 — 全部 PPP 端点的完整 OpenAPI 规范
- 在 VTEX 上集成新的支付服务商 — 从开发到认证的端到端集成指南