mcpField
P
paddle-checkout-web

paddle-checkout-web

github.com/paddlehq開発9121 回インストール5 日前に更新
リポジトリ

paddle-checkout-web について

Next.js App RouterアプリにPaddleのホスト型チェックアウトを追加するスキルです。オーバーレイとインライン表示、イベント処理、顧客情報の事前入力、リダイレクト、カート更新を扱い、サーバー側の関連作業とは分けています。

主な機能

  1. 01オーバーレイまたはインラインの設定
  2. 02イベント処理とリダイレクト
  3. 03認証済み顧客情報の事前入力
  4. 04スロットリング付きカート更新
  5. 05チェックアウト形式と制約の説明

活用シーン

  1. 01Next.jsアプリへのホスト型チェックアウト追加
  2. 02ブランドに合わせたインライン決済画面の構築
  3. 03ログイン済み顧客の情報を事前入力
  4. 04決済中のカート内容の更新

MCP 対応

Paddle MCPサーバーが利用可能な場合、searchとexecuteツールによるアカウントおよびチェックアウト設定の操作方法を説明します。

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/paddlehq/paddle-agent-skills --skill paddle-checkout-web

インストール後、Claude Code で/paddle-checkout-webを入力して読み込まれたか確認します。

詳細

Next.js で Paddle チェックアウトを構築する

このスキルを使う場面

Next.js(App Router)アプリに、オーバーレイ(モーダル形式)またはインライン(ページ内に埋め込み)でホスト型 Paddle チェックアウトを追加する場合に、このスキルを使用してください。初期化、ラインアイテムを指定したチェックアウトの開始、イベントのリッスン、顧客情報の事前入力、チェックアウト後のリダイレクト、カートのような UI 向けのスロットリングされた updateItems パターンを扱います。

このスキルはクライアントサイド専用です。以下と組み合わせてください。

  • サーバーで transaction.completed / subscription.created イベントを受信するには、webhooks。
  • Paddle のサブスクリプション状態をデータベースに反映するには、subscription-sync。
  • Paddle サンドボックスで統合をエンドツーエンドにテストするには、sandbox-testing。

前提条件

以下が必要です。

  • Paddle アカウント。開発中は sandbox 環境を有効にしてください。
  • 商品と価格が少なくとも 1 つ設定されていること。まだ設定していない場合は catalog-setup を使用してください。以下で使用する価格 ID(pri_01h... のような形式)を控えておきます。
  • チェックアウト用に承認されたドメイン。サンドボックスでは、localhost を含む任意のページでチェックアウトが開き、追加したドメインは自動的に承認されます。ライブ環境では、ユーザーが Paddle > My account > Settings > Website approval からドメインを申請します。申請用の API はありません。Paddle MCP サーバーを利用できる場合は、client.checkoutDomains.list()(GET /checkout-domains)で承認状況を確認してください。各ドメインには status があり、チェックアウトを実行できるのは approved のドメインだけです。
  • デフォルトの支払いリンクが設定されていること。localhost ではなく、自分のドメイン上のページを使用してください。ライブ環境では、そのホストがアカウントで承認されたドメイン、またはそのドメインに www. を付けたものである必要があります。サンドボックスではドメインの承認は不要です。サンドボックスでの更新時に checkout_url_domain_not_approved が返された場合は、Paddle > My account > Settings > Website approval からドメインを追加してください。サンドボックスではすぐに承認されるので、その後もう一度試してください。Paddle MCP サーバーを利用できる場合は、client.settings.checkoutUrl.get()(GET /settings/checkout-url)で確認してください。設定されるまでは default_url が null です。ユーザーに確認してから、client.settings.checkoutUrl.update({ default_url: "https://..." })(PATCH /settings/checkout-url、checkout_settings.write 権限が必要)で設定します。利用できない場合、または forbidden が返された場合は、Paddle > Checkout > Checkout configuration > Default payment link から設定するようユーザーに依頼してください。
  • 2 つの環境変数。クライアントサイドトークンは、Paddle > My account > Settings > Authentication の Client-side tokens タブで作成してください。
NEXT_PUBLIC_PADDLE_CLIENT_TOKEN=test_...   # client-side token, safe to expose
NEXT_PUBLIC_PADDLE_ENV=sandbox             # or "production"

Paddle MCP サーバーを利用できる場合は、ユーザーにダッシュボードからコピーしてもらう代わりに、execute 内で client.clientTokens.create({ name: "Frontend dev token" }) を呼び出して、トークンをプログラムから発行できます。clientTokens は camelCase である点に注意してください。トークンの作成には client_token.write 権限が必要です。Paddle CLI を使用する場合は、paddle --environment sandbox client-tokens create --json '{"name": "Frontend dev token"}' --jq '.data.token' を実行してください。

Paddle MCP は、サーバーごとに3つのツール(search、execute、report_missing_tool)を公開します。ワークフロー:search を呼び出して正確なメソッド名とパラメーターの形式を確認し、その後、client.<resource>.<operation>(...) を呼び出す非同期関数を指定して execute を呼び出します。メソッドパスは camelCase(client.clientTokens.create、client.pricingPreview.preview)です。本文パラメーターとレスポンスフィールドは snake_case(tax_category、product_id、unit_price、currency_code)です。リストメソッドは data ではなく、{ pagination: { hasMore, estimatedTotal }, <resource>: [...] } を返します。配列のキーはリソースの API パスセグメントなので、メソッドパスが camelCase の場合は kebab-case になります。例:(await client.products.list()).products、(await client.clientTokens.list())["client-tokens"]。pagination.hasMore が true の間は .next() ではなく、{ after: "<last_id>" } を使ってページを取得します。client.currencies.list() のようにページ分割されないリストは、通常の配列を返します。複数の手順からなるワークフローは1回の execute の中で連結してください。変数は呼び出し間で保持されません。上限:execute ごとに API 呼び出しは50回、タイムアウトは30秒、コードは32,000文字です。

Paddle CLI(paddle、v0.2.0 以降)は、ユーザーが保存したログイン情報を使って、ターミナルから Paddle API の操作を実行します。リソースコマンドは API リファレンスに対応しています。paddle products create は「商品を作成する」コマンドです。コマンドを探すには paddle <resource> --help を実行し、作成または更新コマンドに --schema を追加するとリクエスト本文を確認できます。コマンドがないエンドポイントには paddle api <path> を使います。デフォルトでは本番アカウントで実行されるため、ユーザーが本番環境で作業している場合を除き、--environment sandbox を指定してください。エージェントが実行すると、API の JSON(data、meta)が返され、meta.environment に使用したアカウントが示されます。キャンセルコマンドと削除コマンドには --confirm <id> が必要です。ユーザーがそのキャンセルまたは削除に明確に同意した後にのみ指定し、拒否を回避するために使ってはいけません。paddle api による書き込み操作はまったく確認されないため、同様に扱ってください。終了コード 5 は 403 を示します。権限がないか、別のアカウントでログインしています。終了コード 7 は検証エラーを示します。paddle whoami でログインしていないことがわかった場合は、ユーザー自身で paddle login を実行するよう案内してください(sandbox では API キーを使用します:paddle login --environment sandbox --api-key …)。チャットでキーを尋ねてはいけません。OAuth を使った本番環境へのログインでは、ユーザーの Paddle ロールで許可されているあらゆる変更が可能なため、本番環境での変更内容を一つずつ明示し、実行前にユーザーからの同意を待ってください。CI/CD では、必要な権限だけを付与した本番 API キーを PADDLE_API_KEY として設定してください。インストール方法は Paddle CLI を参照してください。

クライアントライブラリをインストールします:

npm install @paddle/paddle-js

以下のインライン例では、lodash.throttle を使って updateItems の呼び出しを間引いています。このパターンを使う場合は、これと型定義もインストールしてください:

npm install lodash.throttle
npm install -D @types/lodash.throttle

Node SDK(@paddle/paddle-node-sdk)はサーバーサイドでの処理用です。チェックアウトには必要ありません。

チェックアウトのスタイルを選ぶ

スタイル使用する場面ユーザーに表示されるもの
オーバーレイ最も手早く導入できます。現在のページに重ねて開きます。ページ全体を覆うモーダル形式のウィンドウ
インライン周囲のレイアウトやブランディングを完全に制御したい場合に使います。制御する <div> 内にチェックアウトの入力欄が表示されます

どちらのスタイルも同じ Paddle.Checkout.open() 呼び出しを使います。違いは displayMode の設定と、描画される場所だけです。ブランドに合わせたインライン表示が必要でなければ、オーバーレイをデフォルトにしてください。オーバーレイはレイアウトを変更せずに使えますが、インラインにはターゲット要素が必要です。

チェックアウトの種類を選択する

種類説明
One-page顧客情報と支払い情報のすべての入力欄が同じ画面にある、1ページのチェックアウト体験です。
Multi-page2ページのチェックアウトです。1ページ目で顧客情報を収集し、2ページ目で支払い情報を収集します。
Expressエクスプレスウォレット(Apple Pay、Google Pay)用のインラインチェックアウトです。

デフォルトは Multi-page です。ほとんどのユースケースでは One-page をおすすめします。

Express(variant: "express")には追加のルールがあります。

  • インラインのみ(displayMode: "inline")です。
  • 認証済みの顧客(customerAuthToken)、upsell、allowedPaymentMethods、または取引に関連付けられたビジネスと組み合わせると、チェックアウトは開きません。
  • showAddDiscounts、allowDiscountRemoval、showAddTaxId、allowLogout は無視されます。
  • showNonExpressPaymentMethods: false に設定しない限り、その他の支払い方法はウォレットと並んで表示されます(デフォルトは true です)。
  • express には @paddle/paddle-js 1.6.4 以降、showNonExpressPaymentMethods には 1.6.5 以降が必要です。

オーバーレイチェックアウト — 最小限の実用的な統合

最短の方法は、1つの価格のチェックアウトを開くボタンです。おすすめの方法です。

// app/buy/page.tsx
"use client";

import { initializePaddle, type Paddle } from "@paddle/paddle-js";
import { useEffect, useState } from "react";

export default function BuyPage() {
  const [paddle, setPaddle] = useState<Paddle | null>(null);

  useEffect(() => {
    if (!process.env.NEXT_PUBLIC_PADDLE_CLIENT_TOKEN || !process.env.NEXT_PUBLIC_PADDLE_ENV) {
      return;
    }
    initializePaddle({
      token: process.env.NEXT_PUBLIC_PADDLE_CLIENT_TOKEN,
      environment: process.env.NEXT_PUBLIC_PADDLE_ENV as "sandbox" | "production",
    }).then((p) => p && setPaddle(p));
  }, []);

  function openCheckout() {
    paddle?.Checkout.open({
      items: [{ priceId: "pri_01h...", quantity: 1 }],
      settings: {
        variant: "one-page", // or "multi-page"
      },
    });
  }

  return (
    <button onClick={openCheckout} disabled={!paddle}>
      Buy now
    </button>
  );
}

オーバーレイの場合はこれだけです。displayMode の設定は不要です(オーバーレイがデフォルトです)。ユーザーにはモーダルが表示され、支払いを完了すると、Paddle が成功ページを処理します。

イベントを完全に処理するインラインチェックアウト

価格の概要やブランドに合わせたレイアウト、カスタムの成功状態など、他の UI の隣にチェックアウトを表示する必要がある場合は、インラインチェックアウトを使用します。完全なパターンは次のとおりです。

// app/checkout/[priceId]/checkout-contents.tsx
"use client";

import { type Environments, initializePaddle, type Paddle } from "@paddle/paddle-js";
import type { CheckoutEventsData } from "@paddle/paddle-js/types/checkout/events";
import throttle from "lodash.throttle";
import { useParams } from "next/navigation";
import { useCallback, useEffect, useState } from "react";

interface Props {
  userEmail?: string;
}

export function CheckoutContents({ userEmail }: Props) {
  const { priceId } = useParams<{ priceId: string }>();
  const [quantity, setQuantity] = useState(1);
  const [paddle, setPaddle] = useState<Paddle | null>(null);
  const [checkoutData, setCheckoutData] = useState<CheckoutEventsData | null>(null);

  // Throttle updateItems to avoid hammering Paddle on rapid quantity changes.
  const updateItems = useCallback(
    throttle((paddle: Paddle, priceId: string, quantity: number) => {
      paddle.Checkout.updateItems([{ priceId, quantity }]);
    }, 1000),
    [],
  );

  useEffect(() => {
    if (paddle?.Initialized) return;
    if (!process.env.NEXT_PUBLIC_PADDLE_CLIENT_TOKEN) return;

    initializePaddle({
      token: process.env.NEXT_PUBLIC_PADDLE_CLIENT_TOKEN,
      environment: process.env.NEXT_PUBLIC_PADDLE_ENV as Environments,
      eventCallback: (event) => {
        if (event.name && event.data) {
          // Every data event, checkout.loaded included, carries the latest totals.
          setCheckoutData(event.data);
        } else if (event.type === "checkout.warning") {
          // Warnings have no `name`: checkout opened, but some values were dropped.
          console.warn("[Paddle]", event.detail);
        } else if (event.name) {
          // checkout.error / checkout.payment.error: flat { name, type, code, detail, documentation_url }.
          console.error("[Paddle]", event.name, event.code, event.detail);
        }
      },
      checkout: {
        settings: {
          variant: "one-page",
          displayMode: "inline",
          theme: "dark",
          allowLogout: !userEmail,
          frameTarget: "paddle-checkout-frame",
          frameInitialHeight: 450,
          frameStyle: "width: 100%; background-color: transparent; border: none",
          successUrl: `${window.location.origin}/checkout/success`, // must be absolute
        },
      },
    }).then((p) => {
      if (p && priceId) {
        setPaddle(p);
        p.Checkout.open({
          ...(userEmail && { customer: { email: userEmail } }),
          items: [{ priceId, quantity }],
        });
      }
    });
  }, [paddle?.Initialized, priceId, userEmail]);

  useEffect(() => {
    if (paddle?.Initialized && priceId) {
      updateItems(paddle, priceId, quantity);
    }
  }, [paddle, priceId, quantity, updateItems]);

  return (
    <div>
      <PriceSection
        checkoutData={checkoutData}
        quantity={quantity}
        onQuantityChange={setQuantity}
      />
      {/* The class name here MUST match `frameTarget` above. */}
      <div className="paddle-checkout-frame" />
    </div>
  );
}

注目すべき点は3つあります。

  1. frameTarget は CSS クラス名です(先頭にドットは付けません)。ここに設定したクラスと完全に一致する要素もレンダリングする必要があります。Paddle はその要素に iframe を挿入します。
  2. eventCallback はすべてのイベントで呼び出されます。event.name を使ってイベントを判別します(checkout.loaded、checkout.items.updated、checkout.completed、checkout.payment.failed など)。この例では最新のイベントデータを保持し、隣接するコンポーネントで価格の合計を表示できるようにしています。checkout.loaded を含むすべてのデータイベントからデータを取得することで、最初の更新前に正しい合計が表示されます。エラーイベントと警告イベントは異なる形式です。詳しくは、以下の「チェックアウト状態の読み取り」を参照してください。
  3. successUrl はチェックアウト完了後に Paddle がリダイレクトする先です。絶対 http(s):// URL である必要があるため、window.location.origin から作成してください。Checkout.open() に相対 URL を渡すとエラーが発生し、initializePaddle に渡された相対 URL は破棄されます。その場合、チェックアウトは checkout.warning を発行し、リダイレクトしません。購入後の UX(注文確認の表示など)を処理するルートに設定してください。ただし、プロビジョニングには決して依存しないでください。信頼できる情報源は Webhook です。

顧客情報の事前入力

ユーザーがすでに認証済みの場合は、メールアドレスを渡すと、入力の手間を省けます。

paddle.Checkout.open({
  customer: { email: "[email protected]" },
  items: [{ priceId: "pri_01h...", quantity: 1 }],
});

さらに、既存の Paddle 顧客 ID(API 経由で作成したもの、または以前のチェックアウトで返されたもの)を渡すこともできます。

paddle.Checkout.open({
  customer: { id: "ctm_01h..." },
  items: [{ priceId: "pri_01h...", quantity: 1 }],
});

チェックアウト設定で allowLogout: false にすると、事前入力されたセッションからユーザーがサインアウトできないようにできます。ユーザーをすでに認証済みの場合に便利です。

動的な明細項目

チェックアウトを閉じずに数量を変更したり、価格を切り替えたり、別の項目を追加したりするには、Paddle.Checkout.updateItems()を呼び出します。この関数はリスト全体を置き換えるため、リストから省いた項目はすべて削除されます。

paddle.Checkout.updateItems([
  { priceId: "pri_01h...", quantity: 3 },
  { priceId: "pri_02h...", quantity: 1 },
]);
  • 定期購入の項目は、同じ請求サイクルと試用期間である必要があります。
  • 空のリストや、quantityが1未満の項目は無視されます。項目を削除するには、その項目を含めずにリストを送信します。
  • 呼び出す前に、リストがこれらのルールに従っていることを確認してください。

これらの呼び出しは必ずスロットリングしてください(デフォルトでは1秒が適切です。前述のlodash.throttleの例を参照)。Paddleは呼び出しごとにチェックアウトを再描画するため、スロットリングしない更新はちらつきやレート制限のリスクを招きます。

チェックアウト状態の読み取り

eventCallbackを使って独自のUI(合計額、明細項目の内訳、適用済みの割引など)を制御します。特に役立つイベントは次のとおりです。

イベント発生するタイミング一般的な用途
checkout.loadedチェックアウトの初回描画が完了したとき読み込み中のスピナーを非表示にし、初期合計額を設定する
checkout.items.updated明細項目が変更されたとき(updateItems経由を含む)併設する価格サマリーを更新する
checkout.customer.createdチェックアウト中に新しい顧客が作成されたとき新しいcustomer.idを取得する
checkout.payment.selectedユーザーが支払い方法を選択したとき地域に応じた文言を条件付きで表示する
checkout.completed顧客がチェックアウトを完了したとき — 支払いはまだ処理中の場合があります完了メッセージを表示する(アクセス権はWebhookで付与)
checkout.errorチェックアウトの開始時に問題が発生したとき代替手段(mailto、サポートリンク)を提示する
checkout.payment.failed支払いが失敗したとき(カードの拒否、3DS、ウォレット)再試行方法を案内する — 生のエラーは表示しない
checkout.payment.error利用可能な支払い方法がないとき(code: "no_payment_methods_available")代替手段(mailto、サポートリンク)を提示する
checkout.warningチェックアウトは開始したものの、一部の事前入力値や設定値が破棄されたとき不正な入力を検出できるよう、開発中にログへ記録する

エラーイベント(checkout.error、checkout.payment.error)にはdataがありません。name、type、code、detail、documentation_urlはイベントのトップレベルフィールドです(@paddle/paddle-js 1.6.1以降)。checkout.warningにはnameがないため、event.type === "checkout.warning"で判定してください。

全イベントの一覧はdeveloper.paddle.com/paddle-js/eventsを参照してください。

チェックアウト後:リダイレクトとWebhook

チェックアウトが完了すると、次の2つの処理が並行して発生します。

  1. チェックアウトはcheckout.completedを発行し、ユーザーのブラウザーをsuccessUrl(または独自のイベントハンドラー)へ移動させます。一部の支払い方法では、この時点でも支払い処理が続いています。
  2. Paddleはtransaction.completed(および場合によってはsubscription.created)Webhookをサーバーに送信します。

プロビジョニングはリダイレクトやcheckout.completedではなく、Webhookで行ってください。 リダイレクトはユーザー体験(「ご注文ありがとうございます」の表示)のためのものです。一方、Webhookは信頼できる、永続性のある署名付きイベントであり、失敗時には再試行されます。設定方法についてはwebhooksを参照してください。

よくある落とし穴

  • 「Something went wrong」メッセージ — 通常、デフォルトの決済リンクが設定されていないか、本番環境でドメインがチェックアウト用に承認されていないことを意味します。Paddle MCP サーバーを使っている場合は、両方を確認してください。client.settings.checkoutUrl.get()(GET /settings/checkout-url)が default_url を返し、本番環境では client.checkoutDomains.list()(GET /checkout-domains)にその URL のドメインが approved として表示されている必要があります。MCP を使っていない場合は、ユーザーに Paddle > Checkout > Checkout configuration と Paddle > My account > Settings > Website approval を確認してもらってください。
  • Paddle is not defined — Paddle.Checkout.open() を呼び出す前に initializePaddle() を await し忘れています。Promise は Paddle オブジェクトを返します。チェックアウトを開けるのは、その後です。
  • チェックアウトがインラインで表示されない — frameTarget のクラス名がレンダリング済みの要素と一致していないか、Checkout.open() の実行時に要素がまだ DOM にありません。先にターゲットをレンダリングし、useEffect 内で開いてください。
  • サンドボックスと本番環境の不一致 — NEXT_PUBLIC_PADDLE_ENV は、Paddle.js が通信する環境を制御します。クライアントトークンがサンドボックス用なのに NEXT_PUBLIC_PADDLE_ENV=production を設定すると、チェックアウトの読み込みに失敗します。トークンと価格は環境ごとに異なります。サンドボックスの pri_... ID は本番環境には存在しません。
  • クライアントコードに価格 ID を貼り付ける — 手早いプロトタイプには問題ありませんが、実際のアプリではカタログを取得した後にサーバーから価格 ID を読み込んでください。そうすれば、再デプロイせずに差し替えられます。
  • プロビジョニングにリダイレクトや checkout.completed を使う — 決済の処理中に checkout.completed が発火することがあり、ユーザーがタブを閉じたり、接続が切れたり、リダイレクトがブロックされたりすることもあります。信頼できる情報源は Webhook です。
  • 相対パスの successUrl — Checkout.open() は例外をスローし、initializePaddle で設定した場合は破棄され、checkout.warning が表示されるだけです。`${window.location.origin}/checkout/success` を使ってください。
  • updateItems の呼び出し頻度を制限しない — 呼び出し頻度を制限しないと、数量をすばやく変更した際に表示がちらつき、レート制限が適用されることがあります。1 秒が妥当なデフォルト値です。
  • initializePaddle を 2 回呼び出す — 2 回目の呼び出しでは再初期化されず、新しいオプションで Paddle.Update() が呼び出されるため、最初に設定したオプションが置き換えられます。paddle?.Initialized でガードする(例を参照)か、シングルトンパターンを使い、チェックアウトごとの設定は Paddle.Checkout.open({ settings }) に渡してください。

統合を確認する

  1. npm run dev を実行し、チェックアウトページに移動します。
  2. チェックアウトが読み込まれるか iframe が表示され、価格が想定どおりであることを確認します。
  3. Paddle サンドボックスのテストカードを使います。例:有効期限が未来で CVC が 3 桁の任意の数字の 4242 4242 4242 4242。
  4. 購入を完了し、次の点を確認します。
    • ブラウザーが successUrl に移動する。
    • checkout.completed イベントが発火した(eventCallback からの console.log)。
    • Paddle ダッシュボード(サンドボックス)の Transactions に取引が表示されている。
  5. Webhook 側をテストするには、シミュレーターの sandbox-testing を参照してください。

関連ドキュメント

コメント