/checkout(差額課金の開始)

概要

ユーザーがプランを購入/上位プランへアップグレードするときに使うエンドポイントです。
有料時はチェックアウトセッション(Stripe)を生成し、差額のみを請求します。成功時はredirect_url(Stripe Checkout)を返します。フリー配布は別エンドポイント(/licenses/request-free)で処理します。なお、フロントの購入ボタン(.appcotton-buy-button)は、このチェックアウト開始APIを叩く実装が同梱されています。 appcotton-public


エンドポイント

  • HTTP: POST
  • Path: /wp-json/appcotton/v1/checkout/create-session
  • 用途:
    • 新規購入:指定プロダクト+プランのチェックアウトを作成
    • アップグレード:既存ライセンスを指定し、差額のみで上位プランへ

認証

  • ログインユーザー向け:X-WP-Noncewp_create_nonce( 'wp_rest' )
  • ゲスト購入を許可する場合:サーバ側設定による(メール収集はチェックアウト側で実施)

リクエスト(JSON)

{
  "product_id": 123,              // 必須: プロダクトID(または product_slug を許可する実装も可)
  "plan_id": 456,                 // 必須: 対象プランID
  "license_key": "xxxxx",         // 既存ライセンスのアップグレード時に必須
  "success_url": "https://example.com/thanks",
  "cancel_url": "https://example.com/canceled",
  "customer_email": "buyer@example.com" // 任意: ゲスト購入時のプリフィル
}

備考
既存のフロント実装は有料時に { product_id } を送信し、サーバ側でデフォルトプランを選ぶ実装が含まれています。複数プランを正しく選べるよう、plan_id を必須にする方針でクライアント(ショートコード出力 or JS)を拡張してください。 appcotton-public


レスポンス(成功)

{
  "success": true,
  "mode": "payment",                 // or "subscription"
  "redirect_url": "https://checkout.stripe.com/c/pay_cs_..."
}

レスポンス(失敗)

{
  "success": false,
  "error": "plan_not_found",
  "message": "選択したプランが見つかりません。"
}

ステータスコード

  • 200 OK:セッション生成成功
  • 400 Bad Request:入力不備(missing_param, invalid_plan, license_mismatch 等)
  • 401 Unauthorized:認証エラー(invalid_nonce
  • 404 Not Foundproduct_id / plan_id 不在
  • 409 Conflict:同時処理・二重要求(duplicate_checkout
  • 500 Internal Server Error:決済ゲートウェイ等の内部失敗

差額課金(プロレーション)仕様

  • 買い切り→上位買い切り
    請求額 = max( 0, 新プラン価格 - 現プラン価格 )
  • サブスクリプション
    Stripe の**プラン変更の即時課金(proration)**を採用。残存期間の未使用分が自動按分(クレジット or 差額加算)されます。
  • ハイブリッド(買い切り + サブスク)
    いずれか一方のモードへ正規化してから差額算定(設計ポリシーに従う)

実装指針

  • 買い切りは**センチ金額(最小通貨単位)**でDB保存
  • サブスクは**Stripe Price(subscription)**を明示
  • 同一プロダクト内のactivation_limit上限のみ変更するアップグレードは、plan_id差し替え注文レコード生成で整合性を担保

決済完了後(Webhook連携)

Stripe 側イベント(例):

  • checkout.session.completed
  • payment_intent.succeeded(支払い確定)
  • invoice.paid(サブスク更新含む)

AppCotton で行う処理:

  1. 注文状態を paid に更新(新規 or アップグレード)
  2. ライセンスの plan_idactivation_limit対象プラン値に更新
  3. ライセンス状態 active を維持(失効スケジュールがある場合は解除)
  4. 監査ログへ追記(ユーザーID/IP/UA/旧→新プラン)

エラーコード(例)

  • missing_param:必須パラメータ不足(product_id / plan_id / URL)
  • product_not_found:プロダクト不在
  • plan_not_found / plan_not_in_product:プラン不在/プロダクト未紐付け
  • license_not_found:アップグレード対象ライセンスなし
  • license_product_mismatch:ライセンスとプロダクト不一致
  • already_on_plan:既に同一 or 上位プラン
  • stripe_error:ゲートウェイ連携失敗
  • invalid_nonce / unauthorized:認証不正

フロント実装メモ

  • ショートコード

    [AppCotton] エラー: product 属性を指定してください。

    で生成される購入ボタンは、同梱の JS(.appcotton-buy-button ハンドラ)が価格問い合わせ有料なら checkout APIへPOSTの順で処理します。複数プラン表示に対応させるには、プラン選択UIをショートコード側で描画し、選択した plan_id を一緒にPOSTするようにJSを拡張してください。 appcotton-public
  • 無料時は /licenses/request-free を使用(メール入力→即時発行の現在実装)。 appcotton-public

実行例

cURL

curl -X POST "https://YOUR_SITE/wp-json/appcotton/v1/checkout/create-session" \
  -H "Content-Type: application/json" \
  -H "X-WP-Nonce: YOUR_NONCE" \
  -d '{
    "product_id": 123,
    "plan_id": 456,
    "success_url": "https://example.com/thanks",
    "cancel_url": "https://example.com/canceled",
    "license_key": "AC-XXXX-YYYY"   // 既存アップグレード時
  }'

JavaScript(ブラウザ)

const res = await fetch('/wp-json/appcotton/v1/checkout/create-session', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-WP-Nonce': window.appcotton_params?.appcotton_nonce || ''
  },
  body: JSON.stringify({
    product_id: 123,
    plan_id: 456,
    success_url: location.origin + '/thanks',
    cancel_url: location.origin + '/cancel'
  })
});
const data = await res.json();
if (data.success) location.href = data.redirect_url;

PHP(サーバー to サーバー)

$response = wp_remote_post(
  home_url('/wp-json/appcotton/v1/checkout/create-session'),
  [
    'headers' => [
      'Content-Type' => 'application/json',
      'X-WP-Nonce'   => wp_create_nonce('wp_rest'),
    ],
    'body'    => wp_json_encode([
      'product_id'  => 123,
      'plan_id'     => 456,
      'success_url' => home_url('/thanks'),
      'cancel_url'  => home_url('/cancel'),
      'license_key' => 'AC-XXXX-YYYY', // アップグレード時
    ]),
    'timeout' => 20,
  ]
);

Idempotency(冪等)推奨

  • 同一ユーザーが短時間に複数回クリックした場合に備え、license_key + plan_id + user_id などで冪等キーを生成し、Stripe API 呼び出し時に付与してください。
  • API レスポンスには、既存の未使用セッションがある場合はそれを再利用した redirect_url を返すと安全。

テスト

  • Stripe テストモード(公開鍵/秘密鍵)で動作確認
  • テストカード例:4242 4242 4242 4242、任意の将来日、CVC任意
  • Webhook を**受信できる環境(トンネルや Stripe CLI)**で確認し、ライセンス更新の副作用plan_id / activation_limit / 注文レコード)が正しく反映されることを確認

既知の落とし穴

  • フロントが product_id のみを送る旧ロジックのままだと、複数プランがあっても最初のプランが選ばれがち。plan_id 必須化UIのプラン選択対応が必要です。 appcotton-public
  • 無料配布と有料チェックアウトの分岐はクライアント側実装に依存します。価格確認 → 分岐呼び出しの順を維持してください。 appcotton-public

これで、チェックアウト開始 APIの仕様はひととおりです。次に「プラン選択UI」側(ショートコード出力 or ブロック)をplan_id付きでPOSTできるように拡張すると、複数プラン販売と差額アップグレードの導線が揃います。