prava-sdk-integration について
@prava-sdk/coreとPrava Merchant Session APIを使うWebアプリ開発者向けの統合スキルです。埋め込み型とホスト型の決済を扱い、秘密情報や決済認証情報のサーバー側管理、セッション状態、冪等な課金に重点を置きます。
主な機能
- 01Session APIとSDKの統合資料
- 02埋め込み型とホスト型の決済フロー
- 03サーバー側でのセッション作成と状態確認
- 04決済認証情報の保護と結果報告
- 05認証専用Mandateと後続課金
活用シーン
- 01WebアプリへのPrava決済の導入
- 02秘密情報や決済認証情報のブラウザー流出防止
- 03決済状態、冪等な課金、結果報告の処理
- 04Mandate認証と後続課金の設定
インストール
コマンドをコピーしてターミナルで実行します。
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 をウェブアプリケーションに統合します。
このスキルは、@prava-sdk/core と Prava の加盟店向け Session API を使用するアプリケーションを対象としています。Prava CLI を操作する AI エージェントには、別の prava-pay スキルを使用してください。
関連するリソースを読む
- セッション作成、決済結果、ステータスレポート、保存済みカード、または取り消しの呼び出しを実装またはデバッグする場合は、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 キットではありません。
必ず守る不変条件
- どの環境でも、加盟店の秘密鍵はサーバー専用です。 HTML、クライアントの JavaScript、ブラウザーバンドル、または
NEXT_PUBLIC_、VITE_、REACT_APP_で始まる環境変数にsk_*の値を入れないでください。 - 認証済みのアプリケーションサーバーでセッションを作成してください。 ユーザー、金額、通貨、決済先の加盟店、商品は、信頼できるサーバー側の状態から取得してください。生成された
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クエリパラメーターには、JWT ではなく不透明なses_...セッション ID が含まれています。URL を再構築したり、その値をsession_tokenに置き換えたりしないでください。- 決済認証情報はサーバー側に保持してください。 ポーリングではネットワークトークンと動的 CVV が返されます。信頼できるサーバー/エージェントのプロセスで使用し、ブラウザーにシリアライズまたは表示しないでください。
- カスタムチェックアウトでは、
awaiting_resultを認証情報の準備完了状態として扱ってください。 通常、completedになるのは実際のプロセッサーの結果が報告された後です。課金する前にcompletedになるのを待つと、デッドロックが発生します。 - プロセッサーへの課金を冪等にしてください。 認証情報を使用する前に、永続ストレージでその
txn_ref_idをアトミックに確保し、そこから導出した安定したプロセッサー冪等キーを使用してください。ワーカーの再試行では同じプロセッサー処理を再開または照会し、二重に課金しないでください。 - 実際の結果を報告してください。 課金を試行した後、
APPROVEDまたはDECLINEDを指定してreport-statusを呼び出してください。状態を進めるためだけに承認を捏造しないでください。 - ポーリングの前に、承認のみの Mandate を分岐処理してください。
mandate_setup.intent: "mandate_setup"を持つセッションは、後続の課金のための認証を作成します。即時決済の認証情報は発行されません。このセットアップでは、Session API のawaiting_resultを待ったり、Session API のreport-statusを呼び出したりしないでください。 - チェックアウトの試行ごとにセッションを 1 つ作成してください。 iframe と決済結果のポーリング処理では、その 1 回の呼び出しで返された同じ
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
}
]
}
}
カスタムチェックアウトで現在サポートされている購入コンテキストのエントリーは、正確に 1 つです。merchant_details は、連携する AI アプリケーションではなく、認証情報の使用先となる宛先のマーチャントを表します。
見積もりチェックアウト
見積もりモードは、Prava のショップフローで取得した quote_id と組み合わせる場合にのみ使用してください。
{
"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 を指定できるのは、呼び出し元をまたぐフロー向けに見積もりの所有者が明示的に発行した場合のみです。グラントを捏造したり、永続化したりしないでください。見積もりフィールドとカスタムフィールドは相互に排他的です。見積もりモードでは、検証済みの見積もりが正となります。要求された金額と通貨は見積もりと完全に一致する必要があり、一致しない場合は、正規の quoted_amount と currency_iso を含む 409 QUOTE_SESSION_MISMATCH が返されることがあります。
認証のみの委任設定
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 ドメインを使用してください。予約済みまたはローカルの TLD は拒否されます。merchant_details.urlは、IP ホストや埋め込み認証情報を含まない、ICANN に委任されたドメイン上の公開 HTTPS URL である必要があります。パス、クエリ、フラグメントはオリジンに縮約されます。country_code_iso2は、2 文字の大文字で指定してください。currencyは Prava のサポート対象許可リストにある大文字のコードである必要があります。任意の 3 文字の値は使用できません。total_amountには現在、小数点以下が最大 2 桁の、0 以上の 10 進数文字列を指定できます。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. 編集前にアプリケーションを調査する
- フレームワークとパッケージマネージャーを特定します。
- 既存の認証 ID を再利用し、本番ユーザーをハードコードしないでください。
- サーバー側のカート/注文データソースと検証処理を再利用します。
- コンポーネントライブラリ、読み込み/エラーのパターン、スタイルを統一します。
- セッションを作成する前に、フローを埋め込み型にするかホスト型にするか決定します。
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 ビルドでは、登録/トランザクションのライフサイクルイベントは発生しますが、collectPAN() を解決して onSuccess をトリガーする従来の PRAVA_SUCCESS イベントは発生しません。この Promise を await したり、onSuccess を UI/決済状態の条件にしたりしないでください。マウント状態には onReady を、決済状態にはサニタイズされたサーバーポーリングを使用します。アンマウント時に SDK を破棄してください。React Strict Mode では、クリーンアップ時にマウントガードをリセットして、開発時の再マウントで再度初期化できるようにしてください。返された iframe_url はそのまま保持してください。SDK が追加するのは親オリジンのみです。
ホスト型/新しいタブでのチェックアウト
integration_type: "full_checkout" を指定してセッションを作成します。SDK は不要です。
window.open(session.iframe_url, '_blank', 'noopener,noreferrer');
トークンのクエリパラメーターを追加しないでください。返された URL には、不透明なセッション ID がすでに含まれています。
5. サーバーでポーリングして決済を確定する
即時カスタムチェックアウト(見積もりモードではなく、承認のみのマンデート設定でもない場合)における加盟店の決済結果ステートマシンは次のとおりです。
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;
}
ブラウザーにはサニタイズ済みのステータスのみを返してください。トークン、動的 CVV、決済結果の全本文、プロセッサーの承認データをクライアントコンポーネントに返してはいけません。
見積もりチェックアウトでは、Prava が加盟店のチェックアウトを管理し、認証情報を返しません。pending/processing の間はポーリングを続け、トップレベルの completed または failed を終端状態として扱ってください。トランザクションが作成されるまでは transactions は空です。作成後は、認証情報を含まない { txn_id, card_id, status } の概要が含まれます。各概要の card_id は、そのトランザクションに実際に紐付けられたカードとして扱ってください。加盟店のライフサイクルコンテキストは、存在する場合に限り merchant_res として公開されます。失敗情報はトップレベルの error に含まれる場合もあります。見積もりモードでは、認証情報を含む awaiting_result の応答を待ったり、公開したりしないでください。
mandate_setup.intent: "mandate_setup" の場合、この Session API のポーリング経路は使用しないでください。承認フローを完了し、結果のマンデートをサーバー側に保存して、以降の購入にはマンデートの請求/報告ライフサイクルを使用してください。
6. サンドボックスでテストする
https://sandbox.api.prava.spaceとpk_test_*、sk_test_*キーを使用します。- Prava のオンボーディングで割り当てられたネットワークテストカードのみを使用してください。
- 新しいブラウザー/デバイスで初めて実行すると、パスキー登録の前にデバイス紐付け OTP が必要になる場合があります。アカウント用に記載されたサンドボックス OTP を使用してください(標準のサンドボックスコードは
456789です)。 - パスキーには 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 - Vanilla のブラウザー側:
templates/vanilla/integration.html(サーバールートと、ESM 対応のバンドラー/インポートマップと組み合わせてください。秘密鍵は含まれていません)
テンプレートを適用する際は、契約とセキュリティ境界を維持しながら、デモ用のユーザー、注文、加盟店、決済処理業者、レイアウト、エラー表示を、アプリケーションの実装に置き換えてください。
よくある問題
| 症状 | 確認事項 |
|---|---|
purchase_context で 400 VAL_2001 が発生する | 現在のワイヤー形式は { 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() は await せずに開始してエラーを捕捉し、支払いの正当なステータス判定には認証済みのサーバー側ポーリングを使用してください。 |
| 見積もり結果にトランザクション行がない | トランザクションはまだ存在しません。トップレベルのステータス/エラーと任意の merchant_res を使用し、終端状態でない限りポーリングを続けてください。 |
委任設定のポーリングが awaiting_result に達しない | 認証のみの設定では認証情報は発行されません。後続の /v1/mandates/{id}/charge と請求レポートのライフサイクルを使用してください。 |
| 加盟店 URL またはユーザーのメールアドレスが遅れて拒否される | セッション作成時に、公開された HTTPS の ICANN 加盟店オリジンと、メールを送信可能なドメインを使用してください。 |
| キャンセルで UI は非表示になるが、チェックアウトは引き続き機能する | 所有権を確認し、新たな試行を提示する前に、サーバー側のセッション失効エンドポイントを呼び出してください。 |
セキュリティチェックリスト
- 加盟店のシークレットキーは、サーバーのシークレットストアまたは環境変数にのみ保存されています。
- アプリケーションサーバーが呼び出し元を認証し、信頼できる状態からユーザー/注文データを取得しています。
- ブラウザーから呼び出せるステータスリクエストはすべて、永続的なセッション/ユーザーの所有権レコードに基づいて認可されています。
- iframe ホストページに渡されるのは、公開可能キー、セッショントークン、変更されていない iframe URL のみです。
- 決済結果のポーリング、認証情報の使用、ステータスの報告はサーバー側で行われます。
- トークン、動的 CVV、セッション JWT、見積もりへのアクセス許可は、ブラウザー上の表示やログに含まれていません。
- カスタムチェックアウトは、
completedになったときだけでなく、認証情報の準備ができたawaiting_resultの時点で停止します。 - 各
txn_ref_idには永続的なプロセッサーの試行/クレームと、安定したプロセッサーの冪等性キーがあります。 - カスタム決済を試みるたびに、実際の
APPROVEDまたはDECLINEDの結果を報告します。 - 見積もりの処理は、取引前の空の結果と、取引に紐づく
card_idを含む認証情報のない取引サマリーの両方に対応しています。 - 承認のみのマンダト設定は、即時のカスタム認証情報/報告ループに通されません。
- 明示的なキャンセル時は、ローカル状態をリセットする前に、アクティブなセッションをサーバー側で取り消します。
- 本番環境では HTTPS を使用し、環境に対応したライブキー/URL を使用します。
Prava Payments が構築 — AI エージェント向けの決済スタック。