Menu

PayPal統合

PayPalは北米・ヨーロッパ・ラテンアメリカで膨大な既存ユーザーを抱えています。初めて利用するサイトにカード番号を入力するよりも、PayPal残高や連携済みのアカウントで支払いたいというユーザーは少なくありません。Stripeに加えてPayPalを用意しておくことで、そうした層の注文を取りこぼさずに済みます。

NEXTY.DEVのPayPal統合は、2つの販売形態に対応しています。

  • 一度きりのクレジットパック:料金ページにPayPalボタンを表示し、ページを離れることなくポップアップ内で決済が完了します。
  • 継続課金サブスクリプション:ボタンをクリックするとPayPalの承認ページに遷移し、承認後にサイトへ戻ります。

本章では、PayPal側の設定手順と、取得した認証情報をboilerplateに組み込む方法を説明します。

注目すべき点:

  • PayPalはオプションの統合です。Stripeだけで決済を受け付ける場合は、本章をスキップして構いません。
  • PayPalとStripeは同時に有効化できます。どちらの決済チャネルを使うかは、各料金プランのproviderフィールドで決まります。

登録と事前準備

  1. PayPal Businessアカウントを登録します。個人アカウントではサブスクリプション商品を作成できず、商用の支払いも受け取れません。

  2. そのアカウントでPayPal Developer Dashboardにサインインします。開発者ダッシュボードとマーチャントダッシュボードは別々のインターフェースで、本章の作業はほとんどが開発者ダッシュボード側で行われます。

注意:

開発者ダッシュボードの左上にSandbox / Liveの切り替えがあります。開発中はSandboxのまま作業し、決済フローが最後まで通ってから同じ手順をLiveモードでやり直してください。認証情報、Webhook、サブスクリプションプランはSandboxとLiveで完全に独立しており、使い回すことはできません

Appの作成と認証情報の取得

  1. Apps & Credentialsページに移動し、Sandboxモードであることを確認してからCreate Appをクリックし、種別にMerchantを選択します。
paypal-create-app
  1. 作成後にApp詳細ページを開くと、Client IDSecret keyを確認できます(Secretは Show をクリックすると表示されます)。
paypal-app-credentials
  1. これらを環境変数に設定します。
# PayPalのマスタースイッチ。グローバルなPayPalProviderが参照します
NEXT_PUBLIC_ENABLE_PAYPAL=true
# 開発中はsandbox、本番公開時にliveへ変更します
NEXT_PUBLIC_PAYPAL_ENVIRONMENT=sandbox
NEXT_PUBLIC_PAYPAL_CLIENT_ID=your_client_id
PAYPAL_CLIENT_SECRET=your_secret_key

注目すべき点:

NEXT_PUBLIC_PAYPAL_ENVIRONMENTは、boilerplateがどのAPIホスト(api-m.sandbox.paypal.comまたはapi-m.paypal.com)にリクエストするかを決めると同時に、料金設定からtestliveのどちらのプランIDを読むかも決定します。この値と設定したClient IDは必ず同じ環境のものである必要があり、食い違うと認証エラーになります。

Sandboxテストアカウントの作成

Sandboxでは自分の実際のPayPalアカウントで支払うことはできず、PayPalが生成したテストアカウントを使います。

Testing Tools - Sandbox Accountsページを開くと、Businessアカウント(受取側)とPersonalアカウント(支払側)がすでに用意されています。Personalアカウントの**⋮ - View/Edit account**をクリックすればログイン用メールアドレスを確認でき、パスワードも覚えやすいものにリセットできます。

paypal-sandbox-accounts

以降のテストでは、このPersonalアカウントでPayPalの支払いページにサインインします。

Webhookの作成

決済結果、返金、サブスクリプションの更新は、すべてWebhook経由でデータベースに記録されます。この手順は省略できません。

  1. Apps & Credentialsに戻り、作成したAppを開いてWebhooksセクションまでスクロールし、Add Webhookをクリックします。
paypal-webhook-create
  1. Webhook URLを入力します。
  • ローカル開発の場合:トンネルアドレス + APIパス(つまり<your_forwarded_address>/api/paypal/webhook
  • 本番環境の場合:本番環境アドレス + APIパス(つまり<your_server_address>/api/paypal/webhook

トンネルアドレスの取得方法はStripeの章とまったく同じです。CursorまたはVSCodeでForward Portを設定し、ローカルサーバーのポートを入力してPublicに切り替え、生成されたForwarded Addressをコピーします。

forward-port

注意:

PayPalはWebhook URLとしてhttp://localhostを受け付けません。インターネットからアクセス可能なhttpsアドレスが必要です。今すぐトンネリングを用意したくない場合は、ローカルでのWebhook検証は後回しにしても構いません。詳しくは後述の「ローカルでの動作確認」を参照してください。

  1. イベント種別を選択します。boilerplateは以下の15個のイベントに対する処理を実装しているため、すべて選択することを推奨します。
一度きりの支払い(クレジットパック)
- PAYMENT.CAPTURE.COMPLETED
- PAYMENT.CAPTURE.PENDING
- PAYMENT.CAPTURE.DENIED
- PAYMENT.CAPTURE.DECLINED
- PAYMENT.CAPTURE.REFUNDED
- PAYMENT.CAPTURE.REVERSED
 
サブスクリプションの課金
- PAYMENT.SALE.COMPLETED
- PAYMENT.SALE.REFUNDED
- PAYMENT.SALE.REVERSED
 
サブスクリプションのライフサイクル
- BILLING.SUBSCRIPTION.ACTIVATED
- BILLING.SUBSCRIPTION.UPDATED
- BILLING.SUBSCRIPTION.SUSPENDED
- BILLING.SUBSCRIPTION.PAYMENT.FAILED
- BILLING.SUBSCRIPTION.CANCELLED
- BILLING.SUBSCRIPTION.EXPIRED

購読していないイベント種別(例えばCHECKOUT.ORDER.APPROVED)はエラーにならず、そのまま静かにackされます。そのため多めに選択しても副作用はありません。

  1. 保存すると、Webhook一覧にそのWebhookのIDが表示されます。環境変数にコピーしてください。
PAYPAL_WEBHOOK_ID=your_webhook_id

注意:

PAYPAL_WEBHOOK_IDは署名検証に必須のパラメータです。PayPalの検証方式はStripeとは異なり、秘密鍵でローカルにHMACを計算するのではなく、リクエストヘッダーの署名情報とこのWebhook IDをPayPalのverify-webhook-signatureエンドポイントに送り返して検証します。したがってこの値が間違っていたり未設定だったりすると、すべてのWebhookが拒否されます。

料金プランはコードで管理する

具体的な販売形態を設定する前に、前提をひとつ押さえておきます。NEXTY.DEV v4の料金プランはconfig as codeで、すべてconfig/pricing.tsに記述されます。データベースにも保存されませんし、管理画面のCRUDもありません。各プランはproviderフィールドでどの決済事業者が代金を受け取るかを宣言し、PayPalのプランにはprovider: 'paypal'を指定します。

一度きりのクレジットパックと継続課金サブスクリプションでは、PayPal側で必要な準備がまったく異なります

一度きりのクレジットパック継続課金サブスクリプション
PayPal側何も作成する必要なしProductとPlanの作成が必要
料金設定pricecurrencyのみさらにpaypalPlanIdが必要
フロントエンド料金カードに埋め込まれたPayPalボタン、ポップアップ決済PayPal承認ページへ遷移し、承認後に復帰

注意:

一度でも販売したプランは削除せず、idも変更しないでください。 提供を終了する場合はactive: falseに変更します。過去の注文も更新請求もこのslugを辿ってプランを解決するため、削除するとWebhook処理が例外を投げ、PayPalが何日にもわたってリトライを続けることになります。

一度きりの支払い(クレジットパック)の設定

一度きりの支払いでは、PayPalの管理画面で商品や価格を作成する必要はありません。ユーザーがボタンをクリックした時点で、バックエンドがconfig/pricing.tsの金額をもとにOrders API経由で注文を動的に作成します。つまりこの手順はコードの変更だけで完結します。

{
  id: 'pack-mini-paypal',          // 安定したslug。注文とクレジット履歴が永続的に参照します
  kind: 'credit_pack',             // サブスクリプションではなくクレジットパック
  credits: 300,                    // 支払い成功後に付与されるクレジット
  provider: 'paypal',
  price: 5.9,                      // 実際に請求される金額
  currency: 'USD',
  active: true,
  copy: { en: { name: 'Mini Pack', description: '...', features: ['...'] } },
}

ここにはpaypalPlanIdありません。クレジットパックには不要で、指定しても効果はありません。

設定が済むと、料金カードのCTAは自動的にPayPal公式ボタンに切り替わります(条件はprovider: 'paypal'かつkind: 'credit_pack'で、NEXT_PUBLIC_PAYPAL_CLIENT_IDが設定されていることです)。全体の流れは次のとおりです。

ユーザーがPayPalボタンをクリック
  → POST /api/paypal/create-order  バックエンドが設定された金額で注文を作成
  → PayPalのポップアップ内で支払い(ページからは離れない)
  → POST /api/paypal/capture-order  バックエンドがcaptureし、即座にクレジットを付与
  → /payment/success へ遷移

注目すべき点:

  • 金額は常にサーバー側の設定が基準です。 フロントエンドから送られた価格は一切信用しないため、改ざんの心配はありません。
  • currencyはPayPal JS SDKの初期化に使われます。PayPalがサポートする通貨であり、かつマーチャントアカウントが実際に受け取れる通貨である必要があります。
  • boilerplateはデフォルトでカード直接決済を無効化しており(disableFunding: "card")、PayPalアカウント経由の支払いのみを許可しています。有効にしたい場合はlib/paypal/script-options.tsを変更してください。
  • クレジットパックは個人ワークスペースからのみ購入できます。チームワークスペースからの注文はブロックされ、切り替えを促すメッセージが表示されます。
  • ユーザーがeCheckなどの方法で支払った場合、captureはPENDINGを返します。この時点では入金が完了していないため、boilerplateは保留中の注文を記録するだけでクレジットは付与せずPAYMENT.CAPTURE.COMPLETEDイベントの到着を待って付与します。

継続課金サブスクリプションの設定:ProductとPlanの作成

サブスクリプションはクレジットパックとは逆で、販売する前にPayPal側でプランを作成しておく必要があります

PayPalのサブスクリプションは2つのオブジェクトで構成されます。Product(商品) と、価格や課金サイクルを保持するPlan(プラン) です。StripeのProductとPriceの関係とほぼ同じと考えて構いません。

方法1:マーチャントダッシュボードで作成する

  1. Sandbox環境では、前の手順で確認したSandboxのBusinessテストアカウントでsandbox.paypal.comにサインインします。Live環境では、実際のマーチャントアカウントでpaypal.comにサインインします。

  2. Pay & Get Paid - Subscriptionsに移動し、商品を作成します。

paypal-subscription-product
paypal-subscription-product
paypal-subscription-product
  1. その商品に対して、金額・通貨・課金サイクルを指定したプランを作成します。
paypal-subscription-plan
  1. 作成が完了すると、P-5ML4271244454362WXNWU5NQのような形式のプランIDを確認できます。
paypal-plan-id

方法2:APIで作成する

一部の国や地域ではPayPalの管理画面にSubscriptionsメニューが存在しません。その場合はAPIを使うしかありません。

まずClient IDとSecretでアクセストークンを取得します。

curl -v -X POST "https://api-m.sandbox.paypal.com/v1/oauth2/token" \
  -u "CLIENT_ID:CLIENT_SECRET" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials"

商品を作成します。

curl -v -X POST "https://api-m.sandbox.paypal.com/v1/catalogs/products" \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Pro Plan",
    "description": "Nexty Pro subscription",
    "type": "SERVICE",
    "category": "SOFTWARE"
  }'

返却されたidを使ってプランを作成します。

curl -v -X POST "https://api-m.sandbox.paypal.com/v1/billing/plans" \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "product_id": "PROD-XXXXXXXXXXXX",
    "name": "Pro Monthly",
    "status": "ACTIVE",
    "billing_cycles": [
      {
        "frequency": { "interval_unit": "MONTH", "interval_count": 1 },
        "tenure_type": "REGULAR",
        "sequence": 1,
        "total_cycles": 0,
        "pricing_scheme": {
          "fixed_price": { "value": "29.90", "currency_code": "USD" }
        }
      }
    ],
    "payment_preferences": {
      "auto_bill_outstanding": true,
      "setup_fee_failure_action": "CONTINUE",
      "payment_failure_threshold": 3
    }
  }'

レスポンスに含まれるP-で始まるidが、必要なプランIDです。

注目すべき点:

  • total_cycles: 0は無期限の課金を意味し、サブスクリプションではこの設定が適切です。
  • プランの価格は作成後に自由に変更できません。 価格を変えるには新しいプランを作成する必要があります。StripeのPriceと同じ性質なので、料金設計は十分に検討してから作成してください。
  • プランのステータスはACTIVEである必要があります。CREATEDのままのプランでは決済できません。

プランIDを料金設定に反映する

P-で始まるプランIDを取得したら、config/pricing.tsに戻ってサブスクリプションプランのpaypalPlanIdを設定します。

{
  id: 'pro-monthly-paypal',        // 安定したslug。注文とクレジット履歴が永続的に参照します
  kind: 'subscription',
  interval: 'month',
  monthlyCredits: 2000,            // 課金が成功するたびにこの値へリセットされます
  provider: 'paypal',
  paypalPlanId: {
    test: 'P-REPLACE_paypal_sandbox',  // SandboxのプランID
    live: 'P-REPLACE_paypal_live',     // LiveのプランID
  },
  price: 29.9,                     // 表示用の価格。PayPalのプランの金額と一致させる必要があります
  currency: 'USD',
  active: true,
  copy: { en: { name: 'Pro', description: '...', features: ['...'] } },
}

注意:

  • クレジットパックとは異なり、サブスクリプションのprice表示専用です。実際に請求される金額はPayPalのプランが基準になります。両者が食い違うと、ユーザーが見た金額と請求される金額が別物になってしまうため、必ず突き合わせてください。
  • testliveのどちらが読まれるかはNEXT_PUBLIC_PAYPAL_ENVIRONMENTで決まります。両方とも設定しておいてください。

料金設定の詳細については決済システムのドキュメントを参照してください。

ローカルでの動作確認

2つのフローは分けて確認します。いずれもSandboxのPersonalテストアカウントで支払います。

一度きりの支払い

  1. 料金ページを開き、クレジットパックのカードのCTAがPayPal公式ボタンに変わっていることを確認します。ボタンが表示されない場合は、NEXT_PUBLIC_PAYPAL_CLIENT_IDが設定されているか、プランのproviderkindが正しいかを確認してください。
  2. ボタンをクリックし、ポップアップ内でテストアカウントを使って支払います。料金ページから離れることはなく、フロントエンドがcaptureエンドポイントを呼び出した後に/payment/successへ遷移します。
  3. データベースの注文レコードとユーザーのクレジット残高を確認し、金額とクレジット数が設定どおりであることを確かめます。

継続課金サブスクリプション

  1. サブスクリプションプランのボタンをクリックし、PayPalの承認ページ(sandbox.paypal.com/webapps/billing/subscriptions?ba_token=...のようなURL)に遷移することを確認します。遷移しない場合は、paypalPlanIdの設定ミスか、プランがACTIVEでないことがほとんどです。
  2. 承認後に成功ページへ戻り、サブスクリプションのステータスと今期分のクレジットを確認します。
  3. 更新のテストに実際の課金サイクルを待つ必要はありません。次のスクリプトでシミュレートできます。

Webhook

boilerplateはNODE_ENV=developmentのとき署名検証をスキップします(PayPalはlocalhostに検証可能な実イベントを配信できないためです)。したがってローカルでは/api/paypal/webhookに直接イベントをPOSTしてテストできます。サブスクリプション更新をシミュレートするスクリプトも用意されています。

# まずCONFIGブロックのsubscriptionId / userId / planIdなどの項目を書き換えます
node scripts/test-paypal-renewal.mjs
 
# コマンドライン引数で上書きすることもできます
node scripts/test-paypal-renewal.mjs --saleId=SALE-123 --amount=9.99

注意:

PayPalのWebhookはイベントIDによって冪等に処理されます。同じsaleIdで再度送信すると、サーバーは処理済みと判断してスキップします。テストを繰り返すときはIDを変えてください。

Liveモードへの切り替え

Sandboxで一通り動作確認できたら、同じ設定をLiveでやり直す必要があります。以下のリストをチェックリストとして使ってください。

  1. 開発者ダッシュボードをLiveに切り替え、Live用のAppを作成して新しいClient IDとSecretを取得します。
  2. 環境変数を更新します。NEXT_PUBLIC_PAYPAL_ENVIRONMENT=liveに変更し、NEXT_PUBLIC_PAYPAL_CLIENT_IDPAYPAL_CLIENT_SECRETをLiveのものに差し替えます。
  3. Live用のAppで本番ドメインを指すWebhookを作成し直し、同じイベントを選択して、新しいWebhook IDをPAYPAL_WEBHOOK_IDに設定します。
  4. 実際のマーチャントダッシュボードでサブスクリプションのProductとPlanを作成し直し、P-で始まるLiveのプランIDをconfig/pricing.tspaypalPlanId.liveに設定します。一度きりのクレジットパックにこの手順は不要です。 PayPal側にオブジェクトを持たないため、認証情報を差し替えるだけでそのまま動作します。
  5. マーチャントアカウントのメール認証と受取資格の審査が完了していることを確認します。未完了だと注文がPendingのまま止まります。

注意:

LiveのWebhook URLは、サイトが実際に配信されているアドレスと完全に一致している必要があります(wwwのあり・なしは別のアドレスです)。一致しないとイベントが届きません。

Stripeとのいくつかの違い

あらかじめ把握しておくと、つまずきを減らせます。

  • ホスト型の請求ポータルがありません。 StripeにはBilling Portalがありますが、PayPalに相当するものはないため、boilerplateの「サブスクリプション管理」はユーザー自身のPayPal自動支払いページhttps://www.paypal.com/myaccount/autopay/に遷移します。解約はboilerplateがAPI経由で実行します。
  • 1つの請求主体が同時に保持できる有効なサブスクリプションは1つだけです。 プランを変更するにはいったん解約してから購入する必要があり、そうでない場合は決済がブロックされます。
  • 支払いがPending状態になることがあります。 eCheckなどの方法では、PayPalはまず入金前の状態としてPENDINGを返します。このときboilerplateは保留中の注文を記録するだけでクレジットを付与せずPAYMENT.CAPTURE.COMPLETEDの到着を待って実際に付与します。それが届かないままの場合は、/api/cron/creditsの定期ジョブが照合してフォローするので、CRON_SECRETの設定と定期実行のトリガー登録を忘れないでください。
  • クレジットパックはチームワークスペースからは購入できず、個人ワークスペースからのみ購入できます。これはStripeと同じルールです。

まとめ

以上でPayPalの統合は完了です。Liveに切り替える前に、「一度きりの支払い → クレジット付与」「サブスクリプション → 更新 → 解約」「返金 → クレジット回収」の3つの経路をSandboxで一度ずつ通しておくことをおすすめします。