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_plan | monthly、yearly、lifetime など |
premium_expires_at | サブスクの有効期限 |
premium_lifetime | 買い切りなら1 |
stripe_customer_id | Stripe顧客ID |
stripe_subscription_id | StripeサブスクリプションID |
stripe_checkout_session_id | Checkout Session ID |
重要なのは、買い切り判定を is_premium だけで見ないことです。is_premium は「今使えるか」、premium_lifetime は「買い切りか」を表す別の情報として扱います。
4. Checkout作成時はユーザーIDを必ず渡す
決済後に、どのアプリユーザーの決済か分からなくなると困ります。Checkout Session作成時には、client_reference_id と metadata に自アプリの 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を更新できます。
ただし、サブスク情報がすぐに取り切れないケースもあります。実務では次のようにしておくと安心です。
- Checkout Sessionを再取得する
client_reference_idがログイン中ユーザーと一致するか確認するpaymentなら買い切りとして反映するsubscriptionならSubscriptionを取得してcurrent_period_endを保存するcurrent_period_endが一時的に取れない場合、月額なら1か月後、年額なら1年後で仮反映する- 後続のWebhookで正しい期限に補正する
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契約管理ページを開く」のようなボタンを置きます。
9. エラー画面に秘密情報を出さない
Stripe APIエラーをそのまま画面に出すと、キーの一部や内部情報が見えてしまうことがあります。ユーザー向けには簡潔に表示し、詳細は error_log() に残します。
決済画面の作成に失敗しました。
決済設定を確認しています。時間を置いても解消しない場合はお問い合わせください。
本番環境では、APIキー、Webhook secret、リクエスト全文、例外の詳細を画面に出さないようにします。
10. 本番前に必ず試すこと
Stripe導入後は、最低限これを確認します。
- 月額プランを購入できるか
- 年額プランを購入できるか
- 買い切りプランを購入できるか
- 決済後すぐプレミアム化されるか
- DBに
customer_id、subscription_id、checkout_session_idが入るか - サブスク解約後も期限までは使えるか
- 期限切れ後にロックされるか
- Webhookが200 OKを返しているか
prod_とprice_を間違えていないかmk_とsk_を間違えていないか
特に「解約後も期限までは使えるか」は必ず確認した方がいいです。
まとめ
Stripe決済は、Checkout画面を出すだけならそこまで難しくありません。でも実務で大事なのは、その後です。
- どのユーザーの決済か紐づける
- サブスク期限をDBに保存する
- Webhookで更新する
- 解約後も支払い済み期間を守る
- ユーザーが迷わず解約できる導線を用意する
- キーや価格IDを取り違えない
ここまで作って、ようやく「実務で使えるStripe導入」と言えます。特に mk_ と sk_、prod_ と price_、そして「解約イコール即停止ではない」という3点は、Stripe導入時の地雷として覚えておくと役に立ちます。