portaly-product について
自作サイトをクリエイターの Portaly デジタル商品の販売フローにつなぐ開発者向けスキルです。商品表示はサイト側で行い、決済と購入後ページは Portaly がホストし、注文イベントを署名付き Webhook で通知します。
主な機能
- 01API による商品情報の取得
- 02ホスト型チェックアウトセッションの作成
- 03checkoutUrl への購入者リダイレクト
- 04署名付き Webhook イベントの処理
- 05テストモードとライブモード
活用シーン
- 01自作サイトに Portaly 商品を表示
- 02購入者をホスト型決済へ誘導
- 03購入・失敗・返金イベントを受信
- 04テストキーで決済連携を検証
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ portaly-ai/ portaly-skills --skill portaly-productインストール後、Claude Code で/portaly-productを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/api-contract.mdbundle-pricing.mdcallback-signature-v1-vectors.jsoncallback-signature-v1.mdscripts/check_callback_vectors.mjsgo.modsign_callback.mjssign_callback.pysign_callback.webcrypto.mjsverify_callback_test.goverify_callback.go詳細
Portaly デジタル商品の連携
このスキルは、人間のユーザーが自身のウェブサイト(通常は Cursor / v0 / Lovable などでバイブコーディングしたもの)を、クリエイターの Portaly デジタル商品の販売に連携する際に役立ちます。商品の表示 UI はユーザーが用意し、チェックアウト、支払い、メール、注文完了ページは Portaly が担当します。
フローはリダイレクト方式のホスト型チェックアウトです。
- ユーザーのサイトが API 経由でクリエイターの商品を取得し、好みの方法で表示します。
- 購入者が支払いに進む準備ができたら、ユーザーのサイトが API 経由でチェックアウトセッションを作成し、返された
checkoutUrlに購入者をリダイレクトします。 - Portaly が支払いを処理し、購入者に確認メールを送信して、購入後の納品ページを提供します。
- 購入が完了したとき、支払いの試行に失敗したとき、または後日クリエイターが返金したとき、Portaly はユーザーのコールバック URL に署名付きウェブフックを POST します。
Portaly デジタル商品の環境
API ホスト(PORTALY_API_HOST で上書きできます):
https://portaly.ai
| 項目 | 本番モード | テストモード |
|---|---|---|
| API キーのプレフィックス | pcs_live_ | pcs_test_ |
| 実際の請求 | あり | なし — テスト取引のみ |
| チェックアウト時に入力するカード | 購入者の実際のカード | チェックアウトページに表示されるテストカード |
| 完了した注文の保存先 | クリエイターの実際の注文、売上、支払い | 独立したサンドボックス台帳 — 精算処理から切り離されていますが、Payment 管理画面で注文テーブルを「テスト」に設定すると表示されます |
API キーは portaly-payment スキル(クリエイターのサブスクリプション)と共通です。キーは 1 つ、商品は 2 つです。テストキーを使うと、実際のお金を請求せずにチェックアウト全体を実行できます。テストキーで開発し、本番環境ではライブキーに切り替えてください。ただし、テスト購入では 実行されない 処理について、以下のテストモードを確認してください。
支払いはすべて Portaly のホスト型チェックアウトページで処理されます。購入者への請求方法を確認したり、選択したりすることはありません。連携方法は変わらず、商品を一覧表示し、セッションを作成してリダイレクトし、ウェブフックを処理します。
テストモード
カード番号を絶対に作り出さないでください。 テストモードのチェックアウトページでは、使用するテストカードがカード情報欄のすぐ下にある強調表示されたボックスに表示されます。ユーザーには、ページを見て確認するよう伝えてください。このスキルにカード番号が記載されていないのは意図的です。カード番号の唯一の情報源はページです。
そのカードが使えるのは、このスキルが作成する checkoutUrl だけです。 テストモードは商品の属性ではなく、チェックアウトセッションの属性です。同じ商品はクリエイター自身の Portaly ページ(portaly.cc/<slug>)でも販売されており、そちらのチェックアウトは常に実際の決済で、テストモードもテストカードの案内もありません。そこでテストカードを使うと、ゲートウェイのエラーがそのまま返されます(例:Failured3DS)。ユーザーから「テストしたら変なエラーが出た」と報告された場合は、まずどのページで決済したかを尋ねてください。
テストモードでの購入では、決済後の流れは確認できません。 決済処理は実際に実行され(TapPay のサンドボックスに対する処理なので、お金は動きません)、Webhook も実際に発火しますが、注文は別のサンドボックス台帳に記録され、後続の処理はすべてスキップされます。テストを始める前にクリエイターに伝えて、そこに表示されるはずのない情報を探し回らないようにしてください。
- 購入者への確認メールは送信されません。 Portaly の注文メールは本番の注文コレクションに対してのみ送信されるため、確認メールは送信されません。受信トレイを確認するのではなく、独自のハンドラーの出力を検証してください。チェックアウトでメール認証コードが必要な場合、そのコードはテストモードでも引き続き送信されます。ただし、そのメールが届いたからといって、確認メールも届くとは限りません。
- コールバック内の
orderSuccessPageUrlから注文を参照することはできません。 このページでは本番のコレクションのみを検索します。 - 請求書は発行されません — 決済レコードの
invoiceStatus: pendingはそのまま残ります。 - クリエイターの売上、残高、または出金には反映されません。 サンドボックス台帳は決済処理の経路から切り離されています。
- レビュー依頼は送信されません。サンドボックスの注文は意図的にレビューできないためです。
- アフィリエイト報酬は発生しません — ただし、これはテストモードの制限ではありません。この API で作成された注文には、どちらのモードでも報酬は付きません。報酬はサブスクリプションプランの機能です。
portaly-affiliateスキルを参照してください。
テスト注文を確認できる場所。 Portaly Payment 管理画面(https://portaly.cc/admin/creator-subscription)で、注文テーブルの Live/Test 切り替えを Test にすると、これらの注文が一覧表示され、返金もできます。このページにあるタブは Subscriptions と Orders だけなので、「テストタブ」を探すようクリエイターに伝えないでください。テストキーを使えば GET /api/digital-products/orders からも確認できます。クリエイターの通常の売上画面ではなく、この2つの確認方法のどちらかをユーザーに案内してください。
クイックスタート
前提条件 — Portaly Payment アカウントが必要です。 以下のすべての手順で Portaly Payment API キーが必要です。何よりも先に、キーがあるか確認してください(
.env/process.envのPORTALY_API_KEY)。キーがなく、ユーザーがまだ Portaly Payment アカウントを持っていない場合は、作業を中断し、まず https://portaly.cc/payment で登録してもらってください。ページを開くことを提案し、ユーザーが承諾したら開きます。# Portaly Payment の登録ページを開きます。ユーザーの OS に合った行を選んでください Start-Process "https://portaly.cc/payment" # Windows (PowerShell) open "https://portaly.cc/payment" # macOS xdg-open "https://portaly.cc/payment" # Linux登録後、https://portaly.cc/admin/creator-subscription でキーを作成してもらいます。キーが作成されるまで、以下の手順に進まないでください。
- 人間のユーザーが Portaly Payment API キーを持っていることを確認します(上記の前提条件を参照)。開発にはテストキーを推奨します。
- ユーザー自身にシークレットを
.envに配置してもらいます(チャット経由では決して受け取らないでください。ガードレールを参照)。PORTALY_API_KEY=pcs_test_xxx PORTALY_CALLBACK_SECRET=xxx Authorization: Bearer ${PORTALY_API_KEY}を指定して、GET /api/digital-productsで商品を取得します。- 取得した商品フィールドを使って、ユーザー自身のサイトに商品表示 UI を構築します。
- 「チェックアウト」をクリックしたときに、
items[]、totalAmount、callbackUrl、merchantOrderNumberを指定してPOST /api/digital-products/checkout-sessionsを呼び出します。 - レスポンスの
checkoutUrlに購入者をリダイレクトします。 - Webhook 受信処理を実装する前に、リポジトリの言語、フレームワーク、ランタイムを確認してから、
references/callback-signature-v1.mdを読み込みます。一致するアダプターを選択し、本番環境由来のベクトルを実行して、x-portaly-eventが認証済みの本文イベントと一致することを検証し、コールバックのorders[]配列に含まれる各orderIdを永続化します。
API の詳細は references/api-contract.md に記載されています。これらのエンドポイントを呼び出すコードを生成する際は、必ず読み込んでください。
ワークフロー
1. API キーを申請する
portaly-paymentスキルと同じキー取得手順です。すでにこの手順を済ませている場合は、同じキーを再利用し、2つ目のキーを作成しないでください。- シークレットをチャットに貼り付けるようユーザーに求めないでください。
PORTALY_API_KEYとPORTALY_CALLBACK_SECRETをユーザー自身が.envに直接設定するよう伝えてください。 - 続行する前に、
.gitignoreに.envが含まれていることを確認してください。 - シークレットが誤って貼り付けられた場合は、ダッシュボードでローテーションするよう案内してください。
1.5 インストール済みスキルのバージョンを報告する
このスキルのバージョンを Portaly に報告し、インストール済みのスキルが古くなったときに、マーチャントのダッシュボードで警告できるようにします。これはブロックしない単発のバージョン確認です。リクエスト本文に含まれるのはスキル名とバージョンのみで、ユーザーのプロジェクトやデータに関する情報は含まれません。このことをユーザーに一度伝えてください(例:「インストール済みの Portaly スキルのバージョンを報告し、ダッシュボードで古いスキルを検出できるようにしています」)。承認を求めるために作業を中断する必要はありませんが、隠してはいけません。
送信するタイミング — process.env.PORTALY_API_KEY が設定されている場合、次のいずれかが最初に発生した時点で、セッションごとに一度だけ送信します。
- このスキルをインストールまたは更新した直後。すでに環境変数にキーがある場合に限ります。
- それ以外の場合は、Portaly API を初めて呼び出すとき(商品の一覧表示、チェックアウトセッションの作成など)。バージョン報告は、その最初の実際の API 利用に便乗して送信されます。
PORTALY_API_KEYがまだ環境変数に設定されていない場合は、ひとまず報告をスキップしてください(後でケース 2 により報告できます)。送信するためだけにユーザーにキーを求めてはいけません。- リクエスト:
POST https://portaly.ai/api/creator-subscription/skill-version Authorization: Bearer {PORTALY_API_KEY} Content-Type: application/json { "skillName": "portaly-product", "version": "0.10.0" } versionは、このファイルの先頭にあるフロントマター内の、このスキルのmetadata.versionです。実際にインストールされているバージョンが報告されるよう、現在実行している SKILL.md の値をそのまま使用してください。- リクエスト本文には
skillNameとversionのみを含めます。呼び出しに失敗した場合は無視して続行してください。失敗によって処理がブロックされることはありません。 - 成功した場合は、レスポンスの
data.available配列を確認してください。この配列には、マーチャントのアカウントからまだバージョン報告が送信されていないスキルが含まれ、それぞれ{ id, installPackage, latestVersion, description }の形式です。これはインストールされていないという意味ではありません。そのスキル自体のバージョン報告が送信される(初めて実際に使われる)まで、サーバーはそのスキルの存在を認識しません。これはnpx skills addの実行時ではありません。そのため、プロジェクト内にすでに存在していても、まだ使われていないスキルはこの一覧に表示されます。何かを伝える前に、実際のプロジェクトを確認してフィルタリングしてください。スキルのディレクトリまたはファイルがローカルにすでに存在する項目は除外します。サーバーからプロジェクトのツリーは見えず、確認できるのはあなただけです。フィルタリング後に何も残らなければ、何も伝えないでください。残った項目については、追加できるスキルとして、このセッション中に一度だけユーザーに伝えてください(npx skills add ...)。不足しているスキルとして伝えてはいけません。各スキル名と一行のdescriptionを示してください。これは候補の案内に限ります。ユーザーから依頼されない限り、自分でnpx skills addを実行してはいけません。 - このプロジェクトに Portaly のスキルが複数インストールされている場合、それぞれが独自のバージョン報告を送信し、同じ
available一覧を受け取ります。一覧の案内はスキルごとではなく、セッション全体で一度だけ行ってください。このセッションですでにあなた自身、または別の Portaly スキルがavailable一覧を案内している場合は、この手順をスキップしてください。 data.availableが存在しない、空である、またはレスポンスがこの形式に一致しない場合は、何もしないでください。呼び出しが失敗したときと同様に無視して続行します。この手順によって処理がブロックされることはありません。
2. 商品を閲覧する
GET /api/digital-products を使用して、クリエイターが販売している商品を確認します。これは読み取り専用の呼び出しなので、エージェントはユーザーから提供された API キーを使って直接実行できます。サイトで販売する商品をユーザーに確認してください。
コンパクトな一覧レスポンスで、商品グリッド(画像、名前、effectivePrice、price、在庫)を作成できます。タイトルの色や配置、カウントダウンタイマー、複数画像のギャラリー、セール価格のバナーなど、詳細な表示には、GET /api/digital-products/{productId} を呼び出して詳細情報を取得してください。
現在の価格は必ず sale ?? price ではなく、effectivePrice を表示してください。 sale は入力値そのものであり、priceStatus === 'isSale' の場合にのみ有効な価格になります。priceStatus === 'isCountdown' の場合、実際の価格は countdownSetting[0].countdownPrice にあります。productMode === 'free' の場合、価格は 0 です。effectivePrice はサーバー側で計算され、これらすべてのケースに対応済みです。
effectivePrice <= 0 の場合、商品は無料として扱われます。これには、productMode === 'free'、priceStatus === 'isSale' かつ sale === 0、priceStatus === 'isCountdown' かつ countdownPrice === 0、および基準価格の price が 0 の場合が含まれます。無料の商品もバンドルに含められますが、totalAmount には 0 として加算され、請求書が発行されることはありません(§3 を参照)。
2a. 商品カードの表示
詳細ビューには、クリエイターが Portaly の管理画面で設定した項目のほとんどが含まれています。サイトの表示を Portaly のメインの商品カードに似せたい場合は、次の項目を使用してください。
image→ カバー画像title.text(なければname)→ 見出し。title.colorとtitle.alignは、クリエイターが設定したスタイルです。- 価格:
- メインの価格 → 必ず
effectivePriceを使用します(セール/カウントダウン/無料をすでに考慮しています)。 effectivePrice < priceの場合 → 横にpriceを取り消し線付きで表示し、「通常価格」とします。- バッジ(任意の UX):
priceStatus === 'isSale'→ 「セール」バッジ、priceStatus === 'isCountdown'→countdownSetting[]に基づくカウントダウンバナー、productMode === 'free'→ 「無料」ラベル。 sale ?? priceを独自に使わないでください。カウントダウン商品や無料商品を誤って判定します。チェックアウトセッションを作成するときのtotalAmountも同様です。sale ?? priceではなく、effectivePriceを合計してからバンドル割引を適用してください。
- メインの価格 → 必ず
stock+isStock+isShowStock:!isStockの場合 → 在庫を表示しないisStock && stock === 0の場合 → 在庫切れです。CTA ラベルにはstockButtonNameを使用します。isStock && isShowStockの場合 → 残りの数量を表示します。stockは読み取り時点での在庫数であり、取り置きではありません。購入者が支払いを済ませる前に、別の購入者が最後の 1 つを購入する可能性があるため、表示専用として扱い、チェックアウト時の400 OUT_OF_STOCKへの対応を続けてください(§3 を参照)。
buttonName→ CTA ボタンのラベル(未設定の場合は「購入」/「立即購買」がデフォルト)productImages→ 商品詳細ページに表示する追加のギャラリー画像videoUrl/videoImage/videoText→ 任意のプレビュー動画
ユーザーのサイトで購入者に表示する内容(商品表示、カート UI、「チェックアウト」ボタン)はすべてユーザーの責任です。自由にデザインしてください。
2b. 商品の販売停止
通常、クリエイターは Portaly のダッシュボードで商品カタログを管理しますが、サイト独自の販売オン/オフ切り替えが必要な場合は、POST /api/digital-products/{productId}/status に {"isActive": false} を送信すると、その商品の販売を停止できます(再開するには true を送信するか、フィールドを省略して現在の状態を切り替えます)。この操作にはフルアクセスキーが必要です。連携専用に発行されたキーでは 403 KEY_SCOPE_FORBIDDEN が返されます。これは正しい動作であり、回避しようとしてはいけません。
商品の販売を停止すると、新たなチェックアウトが拒否され、一覧エンドポイントから商品が非表示になります。すでに行われた注文には影響しないため、既存の購入者は引き続き納品物を受け取れます。これを「売り切れ」の表現に使用しないでください。在庫はチェックアウト時にすでに確認され、在庫数が限られた商品は在庫がなくなれば自動的に販売を停止します。
詳細な仕様については、references/api-contract.md を参照してください。
3. チェックアウトセッションを作成する
購入者が支払いに進む際、ユーザーのバックエンドから POST /api/digital-products/checkout-sessions を呼び出します。
totalAmount に関するルールは2つあります。
- バンドルに有料商品(
effectivePrice > 0)が1つ以上含まれる →totalAmountは> 0である必要があります。 - バンドルがすべて無料商品(すべて
effectivePrice <= 0)である →totalAmountは厳密に0である必要があります。購入者はカード情報の入力を省略して、そのままチェックアウトに進みます。請求も請求書も発生しません。メール認証は引き続き適用されます。
不一致の場合は 400 TOTAL_AMOUNT_INVALID が返されます。
const res = await fetch(`${HOST}/api/digital-products/checkout-sessions`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.PORTALY_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
items: cart.map(p => ({ productId: p.id })),
totalAmount: cart.totalPrice, // for bundle, your discounted price
currency: 'TWD',
// Only declare `emailVerified` for an email YOU actually verified — never for one
// the buyer just typed into a form. Declaring it removes the emailed code entirely.
...(user?.emailVerified
? {
customerEmail: user.email, // your verified email → buyer skips the code
customerName: user.displayName, // pre-fills the name field (buyer can still edit)
emailVerified: true,
}
: { customerEmail: form.email }), // optional, pre-fill only — buyer still gets the code
callbackUrl: 'https://your-site.com/webhooks/portaly',
successRedirectUrl: 'https://your-site.com/thanks',
cancelRedirectUrl: 'https://your-site.com/cart',
merchantOrderNumber: yourInternalId,
metadata: { tracking: JSON.stringify({ userId: '...', cartId: '...' }) }, // your own data goes under `tracking` as one string (≤ 500 chars) — the Python/Go v1 adapters fail closed on other keys outside the committed schema
}),
})
const { data } = await res.json()
// Redirect the buyer:
return Response.redirect(data.checkoutUrl, 303)
検証は事前に行われます。productId が無効、非アクティブ、または在庫切れの場合、特定の error.code を含む 400 が返されます。実際の決済エラーは、購入者がホスト型ページで支払いを試みた後にのみ発生します(そのページで再試行するか、新しいセッションを開始してください)。
OUT_OF_STOCK は、グリッド上で在庫ありと表示された直後の商品でも発生することがあります。セッションを作成しても在庫は確保されず、その間に最後の1点が別の購入者に売れる場合があります。想定される結果として処理してください。該当商品を更新し、購入者には売り切れたことを伝え、購入者の操作が失敗したかのように表示しないでください。
4. Portaly にホスト型チェックアウトを任せる
ホスト型ページはブラックボックスとして扱ってください。クライアント側でカードトークンを収集しようとしないでください。
ホスト型ページはクリエイターのブランド(クリエイター名、アバター、色)に合わせたデザインで、小さな「powered by Portaly」マークが表示されます。各商品の listedPrice が表示されます。listedPrice < originalPrice の場合は、元の価格に取り消し線が引かれます。totalAmount がすべての listedPrice の合計より小さい場合(たとえば、バンドル割引を適用した場合)、ページには小計、割引額、最終合計が表示されます。
5. Webhook を処理する
POST https://your-site.com/webhooks/portaly
Content-Type: application/json
x-portaly-event: digital_product.checkout.completed
x-portaly-timestamp: 2026-05-19T12:50:00.000Z
x-portaly-signature: <hex>
リポジトリのスタックを確認し、references/callback-signature-v1.md を読み込んで、該当する Node、WebCrypto、Python、または Go のアダプターを使用してください。リリース前に、そのランタイムで scripts/check_callback_vectors.mjs を実行してください。V1 は生の HTTP ボディではなく、${timestamp}.${stableJson(JSON.parse(wireBody))} を PORTALY_CALLBACK_SECRET で検証し、その後 x-portaly-event が認証済みボディ内のイベントと一致することを確認します。
カスタム metadata キーとコールバックの検証: セッション作成時に送信する metadata は、署名付きコールバックボディにそのまま含まれます。V1 は JavaScript の localeCompare でオブジェクトキーをソートしますが、Python および Go のアダプターは任意のキーについてこれを再現できないため、確定済みスキーマに含まれない metadata キーがある場合はフェイルクローズします。独自のデータは、JSON 文字列として tracking キーの下に格納してください(metadata: { tracking: JSON.stringify({ userId, cartId }) })。tracking は検証用ベクターに含まれているため、すべてのアダプターで検証でき、キーではなく値が制約対象なので、1つのキーにあらゆる構造を格納できます。この文字列は各 metadata 値の500文字制限に含まれるため、実際に読み戻すフィールドだけを格納してください。それ以外のカスタムキーを使う場合は、Node/WebCrypto アダプターで検証してください。
次の情報を永続化してください。
sessionId(チェックアウトの冪等性キーとしてeventと組み合わせます。以下を参照)orders[](各要素にorderId、productId、allocatedAmount、orderSuccessPageUrlを含みます)merchantOrderNumbermetadata
x-portaly-timestamp が現在時刻から前後5分を超えているコールバックは拒否してください。 対称の時間枠を設けることで通常のクロックずれを許容できます。一方、未来のタイムスタンプをすべて拒否すると、正当なコールバックが断続的に失敗するおそれがあります。チェックアウトの冪等性には event + sessionId、返金の冪等性には event + orderId を使用してください。すべてのイベントタイプに共通のセッションのみのキーを使わないでください。
購入者には Portaly から自動的にメールが送信されます。注文された商品ごとに1通の購入確認メールが送られ、それぞれの商品に対応する納品物の注文完了ページへのリンクが含まれます。3点のバンドルの場合は、メールが3通別々に届きます(無料商品にはメールが送信されません)。自分でメールを送信する必要はなく、納品物の所有権もありません。テストモードでは、これらのメールは一切送信されません。誰かに受信トレイを確認するよう伝える前に、上記のテストモードを確認してください。
6. 失敗した支払いを処理する(Webhook)
購入者の支払いが拒否されると、Portaly から次の内容が送信されます。
x-portaly-event: digital_product.checkout.failed
ペイロードには sessionId、profileId、merchantOrderNumber、mode、paymentProvider、totalAmount、currency、customerEmail、failureReason、failedAt、metadata が含まれます。注文は作成されないため、orders[] はありません。冪等性キーには event + sessionId を使用してください。test モードのセッションでも送信されます(mode を確認してください)。そのため、ハンドラーをデプロイすると、サンドボックスのエンドポイントにもすぐに通知が届くようになります。
この通知を使って購入者にフォローアップ(再試行リンク、リマインダーメールなど)し、販売機会を黙って逃さないようにしてください。Portaly は失敗したエンドポイントに対し、最大 5 回まで指数バックオフで再試行します。
購入者が支払いをせずにそのまま離れた場合、Webhook は送信されません。該当するセッションの確認方法については、手順 8 を参照してください。
7. 返金を処理する(Webhook)
クリエイターが Portaly の管理画面で注文を返金すると、次の内容が届きます。
x-portaly-event: digital_product.order.refunded
ペイロードには orderId、sessionId、amount が含まれます。返金イベントは注文ごとに発生します。5 個セットが全額返金された場合、order.refunded イベントが 5 件別々に届きます。
この通知を使って、システムで付与した権利を取り消してください(例:ユーザーアクセスの削除、ライセンス数の減算)。
8. 任意:注文と未完了のチェックアウトを一覧表示する
GET /api/digital-products/orders は、この API キーを通じて作成された注文を返します。照合や、ユーザーの管理画面に「購入履歴」パネルを表示する場合に便利です。
GET /api/digital-products/checkout-sessions は、注文に至らなかったものも含むチェックアウトセッションを返します。/orders に保持されるのは購入が成功した注文のみであるため、失敗したチェックアウトや途中で離脱したチェックアウトを確認する唯一の方法です。?outcome=failed|abandoned|pending|completed で絞り込み、?limit=(1~200)と ?startAfter=(pagination.startAfter の値)でページネーションします。このエンドポイントはキー自体ではなく、そのキーが属するアカウントとモードにスコープされるため、キーをローテーションしても履歴は隠れません。
照合時には、?outcome= で絞り込んだリストだけでなく、フィルターなしのリストも確認してください。 outcome フィルターでは表示されない状態が 2 つあります。ゲートウェイのコールバックが失われた決済中のセッションは abandoned として表示され、決済は成功したものの確定されなかった請求は、どの outcome にも一致しません(フィルターなしのリストにのみ表示されます)。どちらも、担当者が確認する必要のある行です。詳しくは references/api-contract.md → 既知の見落としやすいケースを参照してください。
推奨する応答形式
ユーザー向けに実装する場合は、次の内容を返してください。
- 必要な
.envキーの正確な一覧 - 追加が必要なバックエンドエンドポイント(コピーして使えるコード付き)
- Webhook ハンドラーコード(署名検証付き)
- ユーザー側で永続化するデータの最小限のスキーマ(orders テーブル)
- 短いテスト計画。支払いページとして「バックエンドが返す
checkoutUrlを開き、ページに表示されたテストカードで支払います。単品とセットのそれぞれで 1 回ずつ実行し、その後 Portaly 管理画面で注文テーブルを Test に切り替えて、いずれかを返金します。」と記載してください。失敗時の分岐を確認する手順も追加してください。チェックアウトページには拒否されるカードが用意されていないため、実際の決済を失敗させようとするのではなく、scripts/sign_callback.mjsでdigital_product.checkout.failedのペイロードに署名し、自分の Webhook に POST します。
ガードレール
- 開発ではテストモードをデフォルトにしてください。
pcs_live_キーを使うと、実際に課金されるチェックアウトセッションが作成されます。読み込まれたキーがpcs_live_で始まる場合、ライブチェックアウトセッションを作成する前に、ライブモードを意図しているかユーザーに確認してください。購入者を本番環境の請求処理に無断で進ませてはいけません。 - テストモードは商品ではなく、チェックアウトセッションに適用されます。 テストカードが使えるのは、
pcs_test_キーで作成したcheckoutUrlのみです。同じ商品をクリエイター自身の Portaly ページで購入すると、常に実際に課金されます。また、そのページではテストカードを使うと生のゲートウェイエラーが発生します。連携のテスト方法として「Portaly で試してみてください」とユーザーに伝えてはいけません。 - チャットでシークレットを決してそのまま出力しないでください。 ユーザー自身に
.envへPORTALY_API_KEYとPORTALY_CALLBACK_SECRETを設定してもらってください。 - コミットを提案する前に、
.gitignoreに.envが含まれていることを必ず確認してください。 - Webhook ペイロードを処理する前に、必ずWebhookの署名を検証してください。
/webhooks/portalyへの信頼できない POST によって、権限が付与されるおそれがあります。 x-portaly-timestampの有効性を必ず確認してください(現在時刻から前後5分を超える場合は拒否します。この対称的な許容範囲により、一般的なクロックずれに対応できます)。callbackUrlは必ず HTTPS で配信してください。- Webhook の処理では、冪等性キーに
event + sessionId(チェックアウト完了/失敗)およびevent + orderId(返金)を使用してください。Webhook は再配信される場合があります。すべてのイベント種別で同じ session-only キーを使ってはいけません。 - 購入者側の
successRedirectUrlを支払いの証明として信頼してはいけません。 実際にcompleted状態になったことを確認できるのは、Webhook(またはセッションのポーリング)のみです。 metadataにシークレットを入れてはいけません。 Webhook とログにそのまま含まれて返されます。- バンドルの価格設定は自由ですが、クリエイターが提示している合計額を大幅に下回る価格にすると、クリエイターの主要ストアの売上を奪う可能性があります。ライブに移行する前に、クリエイターと相談してください。
成果物
各連携セッションで、以下をユーザーに提供してください。
- ユーザーの在庫を返す、動作する
GET /api/digital-products呼び出し - チェックアウトセッションを作成するバックエンドエンドポイント
- 署名を検証して処理するWebhook受信エンドポイント
- テストモードでエンドツーエンドのテスト(単品購入1件+バンドル購入1件)を実施したことの確認
リソース
references/api-contract.md— エンドポイントの完全な契約:リクエスト/レスポンスの形式、エラーコード、Webhook ペイロード、注文ドキュメントのフィールド。API を呼び出すコードを生成する際は、必ずこれを読み込んでください。references/bundle-pricing.md— 比例配分アルゴリズムと例。references/callback-signature-v1.md— 実行時のルーティング、v1 の厳密な契約、安全なハンドラーの順序、fail-closed の境界、診断ガイダンス。references/callback-signature-v1-vectors.json— コミット済みの本番契約から生成した合成ペイロードと署名。自分で署名と検証を行うフィクスチャの代わりに、これらを使用してください。scripts/check_callback_vectors.mjs— 選択した Node、WebCrypto、Python、または Go のアダプターを、コミット済みの成功・失敗・fail-closed のケースに対して実行します。scripts/sign_callback.mjs— Node.js の HMAC 検証リファレンス(ユーザーのプロジェクトにコピーしてください)。scripts/sign_callback.webcrypto.mjs—node:cryptoをインポートできないエッジランタイム(Cloudflare/Vercel Edge、Deno、InsForge エッジ関数)向けの WebCrypto HMAC 検証リファレンス。同じスキームとバイト単位で同一のstableJsonを使用し、crypto.subtleで検証します。scripts/sign_callback.py— コミット済みのコールバックキー領域に対応する Python アダプター。任意のメタデータキーと未対応の数値は fail-closed で処理します。scripts/verify_callback.goとscripts/verify_callback_test.go— Go アダプター、および本番実装に基づくテストと fail-closed のテスト。