【個人開発】Stripe決済は成功しているのに、有料プランが反映されなかった不具合【Webhook】

個人開発で運営している「虹の世界だより」にて、かなり冷や汗をかく不具合が起きました。

内容としては、Stripeでの決済自体は正常に完了しているのに、サービス側では有料プランとして反映されないというものです。

課金まわりの不具合は、ユーザーさんに直接迷惑をかけてしまう部分です。

なにせ、ユーザーさんはお金を払ってくれてるのに、それが反映されないわけですからね。

今回は、同じようにStripeを使って個人開発サービスを運営している方の参考になるかもしれないので、個人情報や秘匿情報を伏せたうえで、原因と対応内容をまとめておきます。

AD

起きた不具合

ユーザーさんから、「有料プランに申し込んだのに、反映されていない」という連絡をいただきました。

まずStripeの管理画面を確認したところ、決済自体は正常に完了していました。

サブスクリプションも作成されており、請求書の支払いも完了済み。

つまり、Stripe側では問題なく課金が成立している状態でした。

しかし、サービス側のデータベースでは、対象ページのプラン情報が有料プランに更新されていませんでした。

ざっくり言うと、以下のような状態です。

Stripe:決済成功、サブスクリプション作成済み

サービス側DB:無料プランのまま

ユーザー画面:有料機能が使えない

この時点で、原因はStripe Webhookまわりにある可能性が高いと考えました。

原因はWebhookの署名検証失敗

Stripeでは、決済が完了したあとにWebhookという仕組みでアプリ側へイベントを送ってくれます。

たとえば、以下のような情報がWebhookで送られてきます。

  • サブスクリプションが作成された
  • 請求書の支払いが完了した
  • 支払いに失敗した

今回のサービスでは、このWebhookを受け取って、データベース上のプラン情報を更新する仕組みにしていました。

ところが、サーバーログを確認すると、Stripe Webhookの署名検証に失敗していました。

Stripe Webhookは、なりすましを防ぐために、送られてきたリクエストが本当にStripeからのものかを検証します。
その検証に使うのが、Webhook Secretです。

今回、このWebhook Secretが本番環境で正しく読み込まれていませんでした。

ローンチ時点では自分で課金確認を数回行い、問題なく処理できていることは確認したんですが・・・。

そのため、後日の微修正デプロイや環境設定の変更過程で、本番環境の読み込み状態が変わってしまった可能性が高いと考えています。

直接原因は.env.localの残存

詳しく調べていくと、本番サーバー上に.env.localが残っていました。

しかも、その中にローカル検証用のStripe Webhook Secretが入っていました。

本番環境では.envに本番用のWebhook Secretを設定しています。

しかし、PM2の起動設定では、.envを読み込んだあとに.env.localも読み込む構成になっていました。

その結果、.envに正しい本番用Secretが入っていても、あとから読み込まれた.env.localのローカル用Secretで上書きされていたという状態でした。

本番Stripe:本番用の署名でWebhookを送信

アプリ側:ローカル検証用Secretで署名検証

結果:署名が一致せずWebhook処理が失敗

これにより、Stripe側では決済が成功しているのに、アプリ側ではWebhook署名検証が失敗し、DB更新処理が実行されず、有料プランが反映されない状態になっていました。

まず行った緊急対応

まず最初に行ったのは、対象ユーザーさんへの手動反映です。

Stripe側で決済が正常に完了していることを確認できたため、サービス側のデータベースを手動で有料プラン状態に更新しました。

そのうえで、ユーザーさんにはお詫びとともに、ページの再読み込みや再ログインをお願いしました。

課金まわりでは、まずユーザーさんが正しく使える状態に戻すことが最優先ですから。

原因調査も大事ですが、まずは不具合を解消することを最優先ですね。

環境変数の修正

次に、本番サーバー上の.env.localを無効化しました。

その後、PM2を環境変数更新つきで再起動しました。

pm2 restart アプリ名 --update-env

さらに、PM2が実際に正しいWebhook Secretを掴んでいるかも確認しました。

ここで重要なのは、.envを書き換えるだけでは不十分な場合があるということです。

PM2が古い環境変数を保持していると、.env側を直しても、アプリ側では古い値のまま動き続けることがあります。

今回の学び

.envを修正しただけで安心せず、実際にアプリがどの環境変数を掴んでいるかまで確認する必要がある

Stripeイベントを再送して確認

環境変数を修正したあと、Stripe管理画面から失敗していたWebhookイベントを再送しました。

今回確認したのは、主に以下のイベントです。

  • invoice.paid
  • customer.subscription.created

再送後、どちらもHTTP 200で受信できることを確認しました。

これで、Webhookの署名検証が通る状態に戻ったと判断しました。

ただし、WebhookがHTTP 200を返しただけで完全に安心するのではなく、DB側で有料プランとして正しく反映されているかの確認も必須です。

Webhookが成功しても、DBの状態が間違っていたら意味がないですからね…。

あわせてWebhook処理も改善した

今回調査している中で、環境変数とは別に、Webhook処理側も見直す必要があると分かりました。

具体的には、customer.subscription.createdinvoice.paidを、DB反映対象としてきちんと扱う必要がありました。

また、invoice.paidでは、metadataが必ずしもinvoice本体に入っているとは限りません。
明細行やsubscription details側に入っているケースもあるため、metadataの取得処理も見直しました。

見直したポイント

  • どのWebhookイベントをDB更新の対象にするか
  • invoice.paidをどう扱うか
  • metadataをどこから取得するか
  • サブスクリプション状態をどのタイミングでpaid / activeにするか

今回のような課金反映では、単にWebhookを受け取るだけでは不十分です。

どのイベントを正としてDB更新するのか、そして必要な情報をどこから取得するのかを丁寧に見ておく必要があると感じました。

再発防止として入れたこと

再発防止として、デプロイスクリプトにも保護策を入れました。

本番適用時に.env.localが存在する場合、原則として処理を止めるようにしました。

また、デプロイ時に.envや.env.*を本番へコピーしないよう、rsyncの除外対象にも入れています。

今後は、本番環境では.envを正とし、.env.localはローカル専用として扱う方針にします。

さらに、Stripe課金まわりを触ったあとは、最低限以下を確認するようにします。

  • Stripe WebhookがHTTP 200を返しているか
  • PM2が正しい環境変数を持っているか
  • 本番サーバーに.env.localが残っていないか
  • 課金後にDBが正しくpaid / activeになるか
  • 実際の画面で有料機能が使えるか

一度課金確認したから大丈夫、ではなく、デプロイ後にも最低限の確認を入れるべきですね。

今回の学び

今回の件で改めて感じたのは、個人開発の本番運用では、コードだけでなく環境変数やデプロイ手順も含めて「プロダクト」だということです。

コード上は正しくWebhookを受け取るようにしていても、環境変数が違っていれば署名検証は失敗します。

Stripe側で決済が成功していても、Webhook処理が落ちていれば意味がありません。

そして、ユーザーさんから見れば、どこで失敗したかは関係なく、ただ「課金したのに使えない」という体験になってしまいます。

これは、サービス運営者として重く受け止めるべきことだと感じました。

今回の結論

課金まわりは「決済が成功したか」だけでなく、「サービス側で正しく使える状態になったか」まで確認して初めて完了
可能なら定期的に自分で課金して確認するとベター。

決済まわりは本当に慎重に扱う必要がありますね。

おわりに

今回は、Stripe決済は成功しているのに、アプリ側で有料プランが反映されないという不具合を経験しました。

原因は、本番サーバーに残っていた.env.localが、本番用のWebhook Secretを上書きしていたことでした。

対応としては、対象ユーザーさんへの手動反映、.env.localの無効化、PM2の環境変数更新、Stripeイベントの再送、Webhook処理の見直し、デプロイスクリプトへの保護策追加を行いました。

個人開発では、こういう本番運用の失敗も避けて通れないのかもしれません。

ただ、起きた不具合をその場限りで終わらせず、原因を記録し、再発防止まで入れておくことが大事だと感じました。

同じようにStripeを使ってサービスを作っている方の参考になれば幸いです。

AD