关于 prava-sdk-integration
面向使用 @prava-sdk/core 和 Prava Merchant Session API 的 Web 应用开发者,指导接入嵌入式或托管式结账流程。内容重点是服务端密钥与支付凭证隔离、会话状态处理和幂等扣款。
主要功能
- 01提供 Session API 与 SDK 集成参考
- 02支持嵌入式和托管式结账流程
- 03指导服务端创建会话与状态轮询
- 04规范支付凭证保护和结果回报
- 05涵盖授权型 Mandate 与后续扣款
适用场景
- 01在 Web 应用中集成 Prava 结账
- 02避免密钥或支付凭证暴露到浏览器
- 03处理支付状态、幂等扣款及结果报告
- 04配置 Mandate 授权及后续收费
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ prava-payments/ prava-skills --skill prava-sdk-integration安装后在 Claude Code 中输入/prava-sdk-integration确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/evals.jsonrun.tsreferences/integration-flow.mdsdk-api-reference.mdsession-api-reference.mdtest-data.mdtemplates/tsconfig.jsontemplates/express/env.examplesession-route.tstemplates/nextjs/card-form-component.tsxenv.examplepage-integration.tsxserver-action.tstemplates/vanilla/integration.html详细介绍
Prava SDK 集成
将 Prava 集成到 Web 应用中,同时确保原始卡数据、商户密钥或一次性支付凭证不会跨越错误的信任边界。
此技能适用于使用 @prava-sdk/core 和 Prava 商户 Session API 的应用。若要让 AI Agent 操作 Prava CLI,请使用单独的 prava-pay 技能。
阅读相关资源
- 实现或调试 create-session、payment-result、report-status、saved-card 或 revoke 调用时,请阅读 references/session-api-reference.md。
- 挂载 iframe 或处理 SDK 生命周期/事件时,请阅读 references/sdk-api-reference.md。
- 在自定义结账、报价结账、嵌入式和托管式流程之间进行选择时,请阅读 references/integration-flow.md。
- 运行沙盒测试前,请阅读 references/test-data.md。
- 根据应用的框架和设计系统调整
templates/nextjs、templates/express或templates/vanilla下的文件。这些文件是逻辑参考,不是 UI 套件。
不可妥协的约束
- **在任何环境中,商户密钥都只能存在于服务端。**切勿将
sk_*值放入 HTML、客户端 JavaScript、浏览器包或以NEXT_PUBLIC_、VITE_或REACT_APP_为前缀的环境变量中。 - **在经过身份验证的应用服务端创建会话。**从可信的服务端状态中获取用户、金额、货币、目标商户和商品。保存生成的
session_id→ 应用用户/订单绑定,并在每次面向浏览器的状态请求中强制执行此绑定。不要盲目转发浏览器提交的值。 - **使用当前的 create-session
purchase_context对象。**对于POST /v1/sessions,纯数组会被拒绝。自定义结账格式为{ custom: [entry] };报价结账格式为{ quote: true, quote_id, access_grant? }。 - **明确设置呈现方式。**SDK 挂载 iframe 时使用
integration_type: "embedding";托管式/新标签页流程使用integration_type: "full_checkout"。API 默认值为full_checkout。 - **原样传递
iframe_url。**其session查询参数包含不透明的ses_...会话 ID,而非 JWT。切勿重建此 URL 或将该值替换为session_token。 - **将支付凭证保留在服务端。**轮询会返回网络令牌和动态 CVV。请在可信的服务端/Agent 进程中使用这些凭证;不要将其序列化或渲染到浏览器中。
- **对于自定义结账,将
awaiting_result视为凭证已就绪。**通常只有在报告真实的处理器结果后,才会进入completed状态。在扣款前等待completed会导致死锁。 - **确保处理器扣款具备幂等性。**使用凭证前,先在持久化存储中原子地认领其
txn_ref_id,并使用由其派生的稳定处理器幂等键。工作线程重试时必须恢复/查询同一次处理器尝试,绝不能创建第二笔扣款。 - **报告实际结果。**尝试扣款后,使用
APPROVED或DECLINED调用report-status。切勿为了推进状态而伪造批准结果。 - **轮询前,先对仅授权的委托进行分支处理。**带有
mandate_setup.intent: "mandate_setup"的会话会创建供后续扣款使用的授权;它不会立即生成支付凭证。此类设置不要等待 Session API 的awaiting_result状态,也不要调用 Session API 的report-status。 - **每次结账尝试创建一个会话。**iframe 和支付结果轮询器必须使用该次调用返回的同一个
session_id。 - **撤销已放弃的会话。**浏览器中的取消/重新开始操作必须调用经过身份验证的服务端,由其验证会话所有权并调用
POST /v1/sessions/{id}/revoke。关闭 iframe/标签页或清除本地状态不会取消服务端会话。
必填输入
集成前,请收集以下信息:
| 输入 | 沙盒 | 生产环境 | 暴露方式 |
|---|---|---|---|
| 后端 URL | https://sandbox.api.prava.space | https://api.prava.space | 仅服务端使用即可 |
| 发布密钥 | pk_test_* | pk_live_* | 可安全用于浏览器 |
| 商户密钥 | sk_test_* | sk_live_* | 仅服务端使用 |
测试密钥必须与沙盒 URL 搭配使用;正式密钥必须与生产环境 URL 搭配使用。如果无法获取密钥,请添加明显的占位符,并告知用户向 Prava 申请凭证。切勿编造看似可用的凭证。
当前 Session API 合约
自定义结账
POST /v1/sessions 使用 Authorization: Bearer {MERCHANT_SECRET_KEY},请求 JSON 正文如下:
{
"user_id": "user_123",
"user_email": "[email protected]",
"total_amount": "49.99",
"currency": "USD",
"description": "Order 123",
"integration_type": "embedding",
"purchase_context": {
"custom": [
{
"merchant_details": {
"name": "Zara",
"url": "https://www.zara.com",
"country_code_iso2": "US",
"category_code": "5651",
"category": "Apparel"
},
"product_details": [
{
"product_id": "sku_123",
"description": "Ribbed socks",
"unit_price": "49.99",
"quantity": 1
}
],
"effective_until_minutes": 15
}
]
}
}
自定义结账目前恰好支持一个购买上下文条目。merchant_details 描述的是将使用该凭证的目标商户,而不是集成该功能的 AI 应用。
报价结账
仅当 quote_id 是通过 Prava 商店流程获取时,才使用报价模式:
{
"user_id": "user_123",
"user_email": "[email protected]",
"total_amount": "76.00",
"currency": "USD",
"integration_type": "full_checkout",
"purchase_context": {
"quote": true,
"quote_id": "qte_..."
}
}
只有在报价所有者明确为跨调用方流程签发 access_grant 时,才能提供该参数。切勿编造或持久化存储授权凭证。报价字段与自定义字段不能同时使用。在报价模式下,经过验证的报价是权威依据:请求的金额和币种必须与报价完全一致,否则可能返回 409 QUOTE_SESSION_MISMATCH,并附带规范的 quoted_amount 和 currency_iso。
仅授权的委托设置
添加 mandate_setup.intent: "mandate_setup" 会改变生命周期。iframe 会授权并激活一项持续有效的委托,而设置订单和交易会变为 authorized;系统不会为即时扣款生成令牌或 CVV。使用此意图时,请勿运行下方的自定义支付结果循环。
在服务端解析并存储有效委托。后续扣款通过 POST /v1/mandates/{mandate_id}/charge 发起;在服务端使用该扣款的凭证后,将实际结果报告至 POST /v1/mandates/{mandate_id}/charges/{txn_id}/report。这些是委托端点,不是 Session API 的 report-status。具体请求和响应格式见 references/session-api-reference.md。
相关验证规则
- 商户调用方必须发送
user_id和user_email。 user_email必须使用可路由的 ICANN 域名;不接受保留域名或本地域名后缀。merchant_details.url必须是使用 HTTPS 的公开网址,且域名必须由 ICANN 授权;不能使用 IP 主机名或内嵌凭证。路径、查询参数和片段会被精简为来源。country_code_iso2必须是两个大写字母。currency必须是 Prava 支持列表中的大写代码,不能是任意三字母值。total_amount目前接受非负数字字符串,小数部分最多两位。- 如果提供
callback_url,则必须使用 HTTPS。
成功响应
{
"session_id": "ses_...",
"session_token": "eyJ...",
"expires_at": "2026-08-21T12:30:00.000Z",
"iframe_url": "https://sandbox.collect.prava.space?session=ses_...",
"order_id": "ord_..."
}
保留 session_id,用于在服务端轮询。将 session_token 和未经修改的 iframe_url 分别传给 PravaSDK.collectPAN。
集成工作流
1. 编辑前检查应用
- 检测框架和包管理器。
- 复用应用的身份验证身份;不要硬编码生产环境用户。
- 复用服务端购物车/订单数据源及其验证逻辑。
- 沿用应用的组件库、加载/错误处理模式和样式。
- 创建会话前,确定采用嵌入式还是托管式流程。
2. 安装和配置
npm install @prava-sdk/core
Next.js 示例:
PRAVA_BACKEND_URL=https://sandbox.api.prava.space
MERCHANT_SECRET_KEY=sk_test_YOUR_SECRET_KEY_HERE
NEXT_PUBLIC_PUBLISHABLE_KEY=pk_test_YOUR_PUBLISHABLE_KEY_HERE
Express 示例:
PRAVA_BACKEND_URL=https://sandbox.api.prava.space
MERCHANT_SECRET_KEY=sk_test_YOUR_SECRET_KEY_HERE
PRAVA_PUBLISHABLE_KEY=pk_test_YOUR_PUBLISHABLE_KEY_HERE
只有发布密钥需要传到浏览器。
3. 在服务端创建会话
请定义线路类型,而不要接受类型不明确的旧式数组:
type IntegrationType = 'embedding' | 'full_checkout';
interface PurchaseContextEntry {
merchant_details: {
name: string;
url: string;
country_code_iso2: string;
category_code?: string;
category?: string;
};
product_details: Array<{
product_id?: string;
description: string;
unit_price: string;
quantity?: number;
}>;
effective_until_minutes?: number;
}
type PurchaseContext =
| { custom: PurchaseContextEntry[]; quote?: false }
| { quote: true; quote_id: string; access_grant?: string };
面向应用的会话端点应对用户进行身份验证,并根据可信状态构建 API 请求正文:
const response = await fetch(`${PRAVA_BACKEND_URL}/v1/sessions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${MERCHANT_SECRET_KEY}`,
},
body: JSON.stringify({
user_id: authenticatedUser.id,
user_email: authenticatedUser.email,
total_amount: order.total,
currency: order.currency,
external_order_ref: order.id,
integration_type: 'embedding',
purchase_context: { custom: [order.purchaseContext] },
}),
cache: 'no-store',
});
将非机密会话字段返回给页面前,请先验证响应。不要将 MERCHANT_SECRET_KEY 或支付结果载荷发送到浏览器。
4. 渲染结账界面
嵌入式 iframe
使用 integration_type: "embedding" 创建会话,然后挂载返回的 URL,不要对其进行任何修改:
import { PravaSDK } from '@prava-sdk/core';
const prava = new PravaSDK({ publishableKey });
// Start the iframe, but do not use this Promise/callback as payment success.
// Current iframe completion is determined by the server lifecycle below.
void prava.collectPAN({
sessionToken: session.session_token,
iframeUrl: session.iframe_url,
container: '#prava-card-form',
onReady: () => setReady(true),
onChange: (state) => setValidation(state),
}).catch((error) => showError(error.message));
当前 iframe 构建会发出 enrollment/transaction 生命周期事件,但不会发出旧版 PRAVA_SUCCESS 事件,因此不会解析 collectPAN(),也不会触发 onSuccess。不要等待该 Promise,也不要根据 onSuccess 来控制界面或支付状态;使用 onReady 确定挂载状态,并通过服务器端经过清理的轮询来确定支付状态。卸载时销毁 SDK。在 React Strict Mode 中,于清理时重置挂载标记,以便开发环境下重新挂载时可以再次初始化。保留返回的 iframe_url;SDK 只会附加父级来源。
托管式/新标签页结账
使用 integration_type: "full_checkout" 创建会话。无需 SDK:
window.open(session.iframe_url, '_blank', 'noopener,noreferrer');
不要添加 token 查询参数。返回的 URL 已包含不透明的会话 ID。
5. 在服务器上轮询并结算
对于即时自定义结账(非 quote 模式,也非仅授权的 mandate 设置),商户支付结果状态机如下:
pending | processing
↓
awaiting_result (custom checkout credential is ready)
↓ charge through your processor, then report the real result
completed | failed
对于自定义结账,轮询直到在 awaiting_result 状态下出现包含完整凭据的订单项:
type PaymentStatus =
| 'pending'
| 'processing'
| 'awaiting_result'
| 'completed'
| 'failed';
async function waitForCustomCredential(sessionId: string) {
for (let attempt = 0; attempt < 30; attempt += 1) {
const response = await fetch(
`${PRAVA_BACKEND_URL}/v1/sessions/${encodeURIComponent(sessionId)}/payment-result?_t=${Date.now()}`,
{
headers: { Authorization: `Bearer ${MERCHANT_SECRET_KEY}` },
cache: 'no-store',
},
);
const result = await response.json();
if (!response.ok) {
throw new Error(result.error?.message ?? `Payment poll failed (${response.status})`);
}
if (result.status === 'failed') {
throw new Error(
result.error?.message ??
result.transactions?.[0]?.error?.message ??
'Payment failed',
);
}
if (result.status === 'awaiting_result') {
const lineItem = result.transactions
?.flatMap((transaction: { line_items?: unknown[] }) => transaction.line_items ?? [])
.find((item: any) =>
item.token && item.dynamic_cvv && item.expiry_month && item.expiry_year,
);
if (lineItem) return lineItem;
}
if (result.status === 'completed') {
return null; // already settled; do not charge the credential again
}
await new Promise((resolve) => setTimeout(resolve, 3000));
}
throw new Error('Timed out waiting for a payment credential');
}
仅在可信支付流程中使用返回的 token、dynamic_cvv、expiry_month 和 expiry_year。调用处理方之前,先以 txn_ref_id 为键,原子地创建或获取持久化的支付尝试记录,并通过处理方的幂等机制传入从 txn_ref_id 派生的稳定键。持久化处理方请求/引用信息和结果。如果工作进程在授权后、执行 report-status 前崩溃,重试时必须查询或重复同一个幂等的处理方操作,并报告已存储的结果;不得提交新的扣款。仅使用内存锁并不足够。
然后报告处理方实际返回的结果:
async function reportPaymentOutcome(args: {
sessionId: string;
txnRefId: string;
approved: boolean;
authorizationCode?: string;
responseCode?: string;
amountPaid?: string;
}) {
const response = await fetch(
`${PRAVA_BACKEND_URL}/v1/sessions/${encodeURIComponent(args.sessionId)}/report-status`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${MERCHANT_SECRET_KEY}`,
},
body: JSON.stringify({
txn_ref_id: args.txnRefId,
txn_status: args.approved ? 'APPROVED' : 'DECLINED',
...(args.authorizationCode && { authorization_code: args.authorizationCode }),
...(args.responseCode && { response_code: args.responseCode }),
...(args.amountPaid && { amount_paid: args.amountPaid }),
}),
cache: 'no-store',
},
);
const result = await response.json();
if (!response.ok) {
throw new Error(result.error?.message ?? `Report failed (${response.status})`);
}
return result;
}
只向浏览器返回经过清理的状态。绝不要向客户端组件返回 token、动态 CVV、完整支付结果正文或处理方授权数据。
对于 quote 结账,Prava 负责商户结账,并会屏蔽凭据。继续轮询,直到状态不再是 pending/processing;将顶层的 completed 或 failed 视为终态。事务尚不存在时,transactions 为空;事务存在后,其中会包含不含凭据的 { txn_id, card_id, status } 摘要。将每个摘要中的 card_id 视为实际绑定到该事务的卡片。仅当商户生命周期上下文存在时,才会通过 merchant_res 暴露;失败信息也可能位于顶层的 error 中。quote 模式下,不要等待或暴露包含凭据的 awaiting_result 响应。
对于 mandate_setup.intent: "mandate_setup",完全不要使用此 Session API 轮询流程。完成授权流程,在服务器端持久化生成的 mandate,并在后续购买时使用 mandate 扣款/报告生命周期。
6. 在沙盒中测试
- 使用
https://sandbox.api.prava.space,搭配pk_test_*和sk_test_*密钥。 - 仅使用 Prava 入驻流程分配的网络测试卡。
- 在新浏览器/设备上首次运行时,注册 passkey 前可能需要进行设备绑定 OTP 验证;使用账号文档中说明的沙盒 OTP(标准沙盒代码为
456789)。 - 使用 passkey 需要 HTTPS 或 localhost,以及支持 WebAuthn 的浏览器/设备。
- 会话通常约 15 分钟后过期。会话过期后,请创建新会话。
框架模板
- Next.js 服务端/会话逻辑:
templates/nextjs/server-action.ts - Next.js SDK 挂载:
templates/nextjs/card-form-component.tsx - Next.js 页面/状态示例:
templates/nextjs/page-integration.tsx - Express 服务端路由:
templates/express/session-route.ts - 原生浏览器端:
templates/vanilla/integration.html(将其与服务端路由及支持 ESM 的打包器/导入映射表配合使用;其中不包含密钥)
改造模板时,请保留其契约和安全边界,同时将示例用户、订单、商户、处理器、布局和错误呈现替换为应用程序的实际实现。
常见故障
| 症状 | 检查项 |
|---|---|
purchase_context 返回 400 VAL_2001 | 当前的 wire 格式应为 { custom: [...] } 或 { quote: true, quote_id };直接使用数组无效。 |
| 嵌入式流程看起来像托管结账页 | 创建会话时设置 integration_type: "embedding"。 |
轮询时出现 401 | 在路径中使用 session_id,并在服务端使用商户密钥作为 Bearer 身份验证。 |
Next.js 中轮询一直处于 pending 状态 | 添加时间戳查询参数、cache: 'no-store' 和 next: { revalidate: 0 }。 |
轮询到达 awaiting_result 后始终无法变为 completed | 凭据已就绪;请对其扣款并调用 report-status。 |
| 工作线程重试可能导致重复扣款 | 在持久化存储中以原子方式认领 txn_ref_id,并在接触凭据前将其作为处理器幂等性锚点重复使用。 |
iframe 预览返回 404 | 按原样传递 iframe_url。不要将其 ses_... 查询值替换为 JWT。 |
| React 开发模式重新挂载导致 iframe 故障 | 重置 Strict Mode 挂载保护标记,并在清理时销毁 SDK。 |
| 加载旋转指示器一直不消失 | 保留 onReady,并添加 iframe MutationObserver 及有上限的备用超时。 |
collectPAN() / onSuccess 始终无法完成 | 当前 iframe 构建不会触发旧版 PRAVA_SUCCESS 事件。启动 collectPAN() 时不要等待其完成,捕获错误,并使用经身份验证的服务端轮询作为支付状态的权威来源。 |
| 报价结果中没有交易记录 | 交易尚不存在;请使用顶层状态/错误和可选的 merchant_res,除非状态已终止,否则继续轮询。 |
委托设置轮询始终无法到达 awaiting_result | 仅授权的设置不会生成凭据;请使用后续的 /v1/mandates/{id}/charge 和扣款报告生命周期。 |
| 商户 URL 或用户邮箱被延迟拒绝 | 创建会话时,使用公开的 HTTPS ICANN 商户源站和可投递邮件的域名。 |
| 取消操作隐藏了界面,但结账仍可继续 | 验证所有权,并在提供新的尝试选项前调用服务端会话撤销端点。 |
安全检查清单
- 商户密钥仅存在于服务器端密钥存储或环境变量中。
- 应用服务器对调用方进行身份验证,并从可信状态推导用户和订单数据。
- 每个面向浏览器的状态请求都要根据持久化的会话/用户归属记录进行授权。
- 只有可发布密钥、会话令牌和原样的 iframe URL 会传递到 iframe 宿主页。
- 支付结果轮询、凭据使用和报告状态均在服务器端进行。
- 浏览器渲染内容和日志中不包含令牌、动态 CVV、会话 JWT 和报价访问授权。
- 自定义结账流程在凭据就绪的
awaiting_result状态时停止,而不只是等到completed。 - 每个
txn_ref_id都有持久化的处理尝试/认领记录和稳定的处理器幂等键。 - 每次尝试的自定义扣款都要报告其真实的
APPROVED或DECLINED结果。 - 报价处理同时支持事务前的空结果,以及不含凭据且带有交易关联
card_id的交易摘要。 - 仅授权的委托设置不会通过即时的自定义凭据/报告流程处理。
- 显式取消时,在重置本地状态前,先在服务器端撤销有效会话。
- 生产环境使用 HTTPS,以及与环境匹配的正式密钥/URL。
由 Prava Payments 构建——面向 AI Agent 的支付技术栈。