mcpField
P
prava-sdk-integration

prava-sdk-integration

github.com/prava-payments开发878 次安装8 周前更新
代码仓库

关于 prava-sdk-integration

面向使用 @prava-sdk/core 和 Prava Merchant Session API 的 Web 应用开发者,指导接入嵌入式或托管式结账流程。内容重点是服务端密钥与支付凭证隔离、会话状态处理和幂等扣款。

主要功能

  1. 01提供 Session API 与 SDK 集成参考
  2. 02支持嵌入式和托管式结账流程
  3. 03指导服务端创建会话与状态轮询
  4. 04规范支付凭证保护和结果回报
  5. 05涵盖授权型 Mandate 与后续扣款

适用场景

  1. 01在 Web 应用中集成 Prava 结账
  2. 02避免密钥或支付凭证暴露到浏览器
  3. 03处理支付状态、幂等扣款及结果报告
  4. 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.json
run.ts
references/
integration-flow.md
sdk-api-reference.md
session-api-reference.md
test-data.md
templates/
tsconfig.json
templates/express/
env.example
session-route.ts
templates/nextjs/
card-form-component.tsx
env.example
page-integration.tsx
server-action.ts
templates/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 套件。

不可妥协的约束

  1. **在任何环境中,商户密钥都只能存在于服务端。**切勿将 sk_* 值放入 HTML、客户端 JavaScript、浏览器包或以 NEXT_PUBLIC_、VITE_ 或 REACT_APP_ 为前缀的环境变量中。
  2. **在经过身份验证的应用服务端创建会话。**从可信的服务端状态中获取用户、金额、货币、目标商户和商品。保存生成的 session_id → 应用用户/订单绑定,并在每次面向浏览器的状态请求中强制执行此绑定。不要盲目转发浏览器提交的值。
  3. **使用当前的 create-session purchase_context 对象。**对于 POST /v1/sessions,纯数组会被拒绝。自定义结账格式为 { custom: [entry] };报价结账格式为 { quote: true, quote_id, access_grant? }。
  4. **明确设置呈现方式。**SDK 挂载 iframe 时使用 integration_type: "embedding";托管式/新标签页流程使用 integration_type: "full_checkout"。API 默认值为 full_checkout。
  5. **原样传递 iframe_url。**其 session 查询参数包含不透明的 ses_... 会话 ID,而非 JWT。切勿重建此 URL 或将该值替换为 session_token。
  6. **将支付凭证保留在服务端。**轮询会返回网络令牌和动态 CVV。请在可信的服务端/Agent 进程中使用这些凭证;不要将其序列化或渲染到浏览器中。
  7. **对于自定义结账,将 awaiting_result 视为凭证已就绪。**通常只有在报告真实的处理器结果后,才会进入 completed 状态。在扣款前等待 completed 会导致死锁。
  8. **确保处理器扣款具备幂等性。**使用凭证前,先在持久化存储中原子地认领其 txn_ref_id,并使用由其派生的稳定处理器幂等键。工作线程重试时必须恢复/查询同一次处理器尝试,绝不能创建第二笔扣款。
  9. **报告实际结果。**尝试扣款后,使用 APPROVED 或 DECLINED 调用 report-status。切勿为了推进状态而伪造批准结果。
  10. **轮询前,先对仅授权的委托进行分支处理。**带有 mandate_setup.intent: "mandate_setup" 的会话会创建供后续扣款使用的授权;它不会立即生成支付凭证。此类设置不要等待 Session API 的 awaiting_result 状态,也不要调用 Session API 的 report-status。
  11. **每次结账尝试创建一个会话。**iframe 和支付结果轮询器必须使用该次调用返回的同一个 session_id。
  12. **撤销已放弃的会话。**浏览器中的取消/重新开始操作必须调用经过身份验证的服务端,由其验证会话所有权并调用 POST /v1/sessions/{id}/revoke。关闭 iframe/标签页或清除本地状态不会取消服务端会话。

必填输入

集成前,请收集以下信息:

输入沙盒生产环境暴露方式
后端 URLhttps://sandbox.api.prava.spacehttps://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 的支付技术栈。

评论

支持 Markdown