Copicode 日本語トップ

PHPアプリにStripe決済を導入する時の実務ポイント

PHPアプリにStripe決済を入れる時、公式ドキュメントどおりに進めているつもりでも、実務では意外なところで止まります。

特にサブスクリプションを扱う場合は、「決済画面を出す」だけでは不十分です。決済成功後のDB反映、Webhook、解約後の利用期限、ユーザー向けの解約導線まで考えておかないと、あとで危ないバグになります。

この記事では、月額サブスク、年額サブスク、買い切りプランをPHPアプリへ導入する時に、実務で気を付けるべきポイントを整理します。

Stripeのテスト結果をAIへ相談する前に

Checkout、Webhook、DB反映、解約後の期限、Customer Portalのどこで止まっているかを整理したい時は、Stripe決済確認メモメーカーで相談文を作れます。

Secret key、Webhook signing secret、顧客情報、請求情報はAIへ貼らないでください。

1. sk_ と mk_ を間違えない

StripeのAPIキー画面には、キー本体とは別に「APIキーID」があります。ここで紛らわしいのが、mk_... から始まるIDです。これはアプリに設定する秘密鍵ではありません。

サーバー側で使う秘密鍵は、基本的に次の形式です。

sk_test_...
sk_live_...

公開可能キーは次の形式です。

pk_test_...
pk_live_...

Stripeの本番シークレットキーは一度しか完全表示できません。見失った場合は、古いキーをローテーションして新しい sk_live_... を発行します。

参考: Stripe API keys

2. prod_ ではなく price_ を使う

Checkout Sessionで渡すのは商品IDではなく価格IDです。

間違いの例です。

define('STRIPE_PRICE_MONTHLY', 'prod_...');

正しくは、price_... から始まる価格IDを使います。

define('STRIPE_PRICE_MONTHLY', 'price_...');
define('STRIPE_PRICE_YEARLY', 'price_...');
define('STRIPE_PRICE_LIFETIME', 'price_...');

Stripeでは、商品そのものが prod_...、その商品をいくらで売るかが price_... です。月額100円、年額1000円、買い切り3000円のように複数プランがあるなら、それぞれの価格IDを設定します。

3. DBにはStripe連携用の列を用意する

最低限、ユーザーテーブルには以下のような列があると管理しやすいです。

is_premium TINYINT(1) DEFAULT 0,
premium_plan VARCHAR(20) NULL,
premium_expires_at DATETIME NULL,
premium_lifetime TINYINT(1) DEFAULT 0,
stripe_customer_id VARCHAR(255) NULL,
stripe_subscription_id VARCHAR(255) NULL,
stripe_checkout_session_id VARCHAR(255) NULL
列名意味
is_premium現在プレミアムとして有効か
premium_planmonthlyyearlylifetime など
premium_expires_atサブスクの有効期限
premium_lifetime買い切りなら1
stripe_customer_idStripe顧客ID
stripe_subscription_idStripeサブスクリプションID
stripe_checkout_session_idCheckout Session ID

重要なのは、買い切り判定を is_premium だけで見ないことです。is_premium は「今使えるか」、premium_lifetime は「買い切りか」を表す別の情報として扱います。

4. Checkout作成時はユーザーIDを必ず渡す

決済後に、どのアプリユーザーの決済か分からなくなると困ります。Checkout Session作成時には、client_reference_idmetadata に自アプリの user_id を入れておくと追跡しやすくなります。

$post_fields = [
    'line_items' => [[
        'price' => $price_id,
        'quantity' => 1,
    ]],
    'mode' => $mode,
    'success_url' => $success_url,
    'cancel_url' => $cancel_url,
    'client_reference_id' => $_SESSION['user_id'],
    'metadata' => [
        'user_id' => $_SESSION['user_id'],
        'plan' => $plan,
    ],
];

サブスクなら subscription_data.metadata にも入れます。

$post_fields['subscription_data'] = [
    'metadata' => [
        'user_id' => $_SESSION['user_id'],
        'plan' => $plan,
    ],
];

買い切りなら payment_intent_data.metadata にも同じ情報を入れておくと、後続の確認がしやすくなります。

5. success.phpだけに頼りすぎない

決済後、ユーザーは success.php に戻ってきます。そこで session_id を使ってStripeへ確認し、アプリ側DBを更新できます。

ただし、サブスク情報がすぐに取り切れないケースもあります。実務では次のようにしておくと安心です。

success.php はユーザー体験を良くするための即時反映に使い、最終的な整合性はWebhookで担保する、という考え方が安全です。

6. Webhookは必須

サブスクを扱うならWebhookは必須です。最低限、以下のイベントに対応しておきます。

checkout.session.completed
customer.subscription.created
customer.subscription.updated
customer.subscription.deleted
invoice.payment_succeeded
invoice.payment_failed

Webhookでは署名検証を必ず行います。Webhook signing secretはAPIキーではなく、whsec_... から始まる別の値です。

参考: Stripe Webhooks

7. 解約しても即ロックしてはいけない

サブスクを解約したからといって、すぐにプレミアム機能を止めてはいけません。月額料金や年額料金を支払ったユーザーは、支払い済み期間の終了日までは使えるべきです。

やってはいけない実装です。

// 解約イベントが来たら即停止してしまう例
is_premium = 0;
premium_expires_at = NOW();

正しい考え方は、current_period_end が未来ならその日までは is_premium = 1 を維持し、期限を過ぎたら is_premium = 0 にすることです。

customer.subscription.deleted を受けても、Stripe側の current_period_end が未来ならプレミアムを維持します。ここを間違えると「解約した瞬間に使えなくなった。まだ期間が残っているのに」となり、ユーザーから見ればかなり不誠実に見えます。

8. ユーザーにStripe管理画面ログインを案内しない

解約導線にも注意が必要です。ユーザーはStripeの管理者ダッシュボードにログインするわけではありません。アプリ側からStripeのCustomer Portal、つまり顧客向け契約管理ページへ飛ばすのが正しい導線です。

Customer Portalでは、ユーザーが支払い方法、請求書、サブスクリプション、解約などを管理できます。

PHP側では、ログイン中ユーザーの stripe_customer_id を使ってPortal Sessionを作ります。

curl_setopt($ch, CURLOPT_URL, "https://api.stripe.com/v1/billing_portal/sessions");
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
    'customer' => $customer_id,
    'return_url' => SITE_BASE_URL . "/premium.php",
]));

アプリ内には、分かりやすく「Stripe契約管理ページを開く」のようなボタンを置きます。

参考: Stripe Customer Portal

9. エラー画面に秘密情報を出さない

Stripe APIエラーをそのまま画面に出すと、キーの一部や内部情報が見えてしまうことがあります。ユーザー向けには簡潔に表示し、詳細は error_log() に残します。

決済画面の作成に失敗しました。
決済設定を確認しています。時間を置いても解消しない場合はお問い合わせください。

本番環境では、APIキー、Webhook secret、リクエスト全文、例外の詳細を画面に出さないようにします。

10. 本番前に必ず試すこと

Stripe導入後は、最低限これを確認します。

特に「解約後も期限までは使えるか」は必ず確認した方がいいです。

まとめ

Stripe決済は、Checkout画面を出すだけならそこまで難しくありません。でも実務で大事なのは、その後です。

ここまで作って、ようやく「実務で使えるStripe導入」と言えます。特に mk_sk_prod_price_、そして「解約イコール即停止ではない」という3点は、Stripe導入時の地雷として覚えておくと役に立ちます。

関連して読む記事