🔥

【Stripe × 3Dセキュア】無料トライアル後に自動課金が失敗する対策

に公開

🧩 【自前フォーム実装向け】 無料トライアル後にStripeの自動課金が失敗する理由と対策

SaaSのサブスクでは「無料トライアル → 自動課金」は定番パターンです。
しかし、トライアル終了時の初回課金がなぜか失敗することがあります。

特に「3Dセキュア(本人認証)が必要なカード × Stripeサブスクリプション × 自前フォーム」構成では、
正しく動かないケースが起きがちです。

本記事では、次のポイントを整理します

  1. なぜ無料体験後に課金が失敗するのか

  2. セキュリティ的な問題はあるのか

  3. どう実装すれば安定するのか

💡 前提:UI/UXを重視した自前フォーム構成

本記事は Stripe Checkoutではなく、自前フォームでカード情報を登録するケースを前提にしています。

「Checkout使えば早いのに、なぜ自前フォーム?」

という疑問もあるかもしれません。
自前フォームを選ぶ理由は主に以下のようなUI/UX上の狙いです。

ブランド体験を統一したい(Checkoutの画面に遷移せず、アプリ内で完結させたい)

ユーザー入力を最小限にしたい(住所や請求情報を既存データから自動入力したい)

サブスク登録〜無料体験開始までをシームレスにしたい(学習・プロダクト体験を途切れさせたくない)

こうした理由から、フロント側で Elements を使ってカード情報を直接扱う実装を採用していました。

しかし、このUI/UX優先の構成が思わぬ落とし穴を生みました。
しかも――Stripeのテスト環境では再現せず、本番で初めて発覚したのです。

⚠️ テスト環境では再現しなかった理由

Stripeのテストモードでは、多くのテストカードが「3Dセキュア不要」または「成功扱い」として動作します。
そのため、開発段階では下記のように動作して問題がないように見えていました。

カード登録:成功(setupIntent.status = succeeded)

トライアル終了後:自動課金も正常完了

しかし、本番環境では「実際のカードが3Dセキュア認証を要求」するケースがあり、
その結果、トライアル終了時の課金がブロックされてしまいました。

✅ テスト環境では通る
❌ 本番では3Dセキュア未完了扱いで課金失敗

このギャップが、今回のバグを見逃していた最大の原因でした。

💥 原因:default_payment_method が設定されていない

ユーザーがカード情報を入力すると、フロントで
stripe.confirmCardSetup() を実行し、setupIntent.status が返ります。

const result = await stripe.confirmCardSetup(clientSecret, {
  payment_method: {
    card: cardElement,
    billing_details: { name: 'Taro Yamada' },
  },
});

// result.setupIntent.status
// succeeded → 認証完了
// requires_action → 3Dセキュア未完了

この時、取得した payment_method をサブスクリプションに紐づけておく必要があります。

ところが自前フォームでは次のような実装ミスがよくあります:

カード情報は保存したけど、サブスクに default_payment_method を設定していない。

結果、Stripeは「どのカードで請求すればいいか分からない」状態となり、
トライアル終了後の初回課金でエラーになります。

🔑 対策:default_payment_method を設定する

サブスクリプション作成時に、必ず
取得した payment_method.id を default_payment_method に設定しましょう。


const subscription = await stripe.subscriptions.create({
  customer: customerId,
  items: [{ price: priceId }],
  default_payment_method: paymentMethodId, // ←ここが重要
  trial_period_days: 14,
});

これにより:

サブスクと支払い手段が正しく紐づく

トライアル終了後に自動で課金される

「支払い手段なし」エラーを防げる

という安定した動作になります。

🔒 3Dセキュア未完了カードの挙動

では「3Dセキュア未完了」のカードも default_payment_method に設定して大丈夫でしょうか?

Stripeの挙動はこうです。

状態 挙動
3Dセキュア不要 or 認証済み ✅ 課金成功
3Dセキュア未完了 ❌ 課金失敗(Stripeがブロック)

つまり、トライアル中にsetupIntentが requires_action のままだと、
無料体験は通っても、課金時に弾かれます。

💬 実装ポイントまとめ

項目 内容
カード登録時 stripe.confirmCardSetup() で payment_method を取得
サブスク作成時 default_payment_method にそのIDを指定
3Dセキュア未完了時 ユーザーに再認証を促すUIを用意
無料体験ありの場合 課金タイミングが遅れる点を考慮する
UI/UX観点 自前フォームでシームレスな体験を実現
テスト観点 テスト環境では再現しない3Dセキュア挙動に注意

📝 まとめ

・default_payment_method を必ず設定する
・3Dセキュア未完了をユーザーに明示しておく

Stripeのテスト環境は非常に優秀ですが、
3Dセキュア周りは実カードでしか再現できないパターンが存在します。
UI/UXを重視して自前フォームを選ぶなら、
本番検証フェーズでの動作確認が欠かせません。

Discussion