Stripeでサブスクリプション課金を組むとき、難しいのは決済画面を出すところではありません。詰まるのは、プラン変更・支払い失敗・解約という「契約が動いたあと」の状態を、自分のデータベースにどう反映するかです。決済画面まではドキュメントどおりに書けば半日で動きますが、そのあとに来る状態変化は、Webhookを正しく設計していないと静かに取りこぼされます。
取りこぼしは目に見えるエラーになりません。解約した利用者が有料機能を使い続けたり、逆に払っている利用者が無料プランに落ちたりします。どちらも例外を投げないので、ログを見ても気づけません。気づくのは利用者から問い合わせが来たときです。
この記事では、プラン設計・カスタマーポータルへの委譲・拾うべきWebhookの3イベント・支払い失敗時の扱い・日割りの挙動を、Stripe公式ドキュメントの記述と対応させながら整理します。最後に、テストモードから本番へ切り替える前に確認する5項目を合否ラインつきで置きます。Checkout Sessionと署名検証の実装コードそのものは扱いません(後述の記事に譲ります)。
サブスク課金で詰まるのは、決済のあとに来る3つの状態変化
1回限りの決済と違い、サブスクリプションは契約が生き物です。決済が終わったあとも、契約の状態は勝手に変わり続けます。アプリ側が追いかけなければならない変化は、大きく3つしかありません。
| 状態変化 | 何が起きたか | アプリ側がやること |
|---|---|---|
| プランが変わる | 利用者がアップグレード/ダウングレードした | 保存しているプラン名を書き換える |
| 払えなくなる | カードの期限切れ・残高不足で請求が失敗した | 猶予期間を与え、期限を過ぎたら機能を止める |
| やめる | 利用者が解約した/リトライを尽くして打ち切られた | 無料プランに戻し、契約IDを外す |
重要なのは、この3つはいずれも自分のアプリを経由せずに起きうるという点です。利用者はStripeが用意した画面で解約できますし、カードの期限切れは誰の操作もなく訪れます。だからこそ、状態の変化はWebhookで受け取るしかありません。自分の画面に解約ボタンを置いたかどうかとは無関係に、Webhookの設計が課金の正しさを決めます。
なお、サーバーやAPIの役割そのものが曖昧な状態で課金に手を付けると切り分けが難しくなります。全体像から確認したい場合はバックエンドとは?サーバー・DB・APIの全体像を先に読んでおくと、この記事の話が置き場所付きで入ります。
最初に決める3つ:Priceの切り方・プラン数・値上げしたときの扱い
コードを書く前に決めておかないと、あとで作り直しになる設計項目が3つあります。いずれも「一度売り始めると変えにくい」ものです。
ProductとPriceは「商品」と「値札」の関係
Stripeでは、売るもの自体をProduct、いくらでどの周期で請求するかをPriceとして分けて持ちます。「Proプラン」がProductで、「月額2,000円」「年額20,000円」がそれぞれ別のPriceです。月払いと年払いを用意するなら、Productは1つ、Priceは2つになります。
アプリ側は、このPriceのIDを見て「この人はどのプランか」を判定することになります。判定の入口がPrice IDである以上、Price IDが増えたり入れ替わったりする場面をあらかじめ想定しておく必要があります。それが次の項目です。
値上げすると、Price IDは必ず増える
Priceの金額は、あとから書き換える運用にはなっていません。値段を変えるときは新しいPriceを作り、そこから先の契約を新しいPriceに向けます。つまり値上げした瞬間、システムの中には「旧価格のPrice ID」と「新価格のPrice ID」が同時に存在します。既存の契約者は旧IDのまま残ります。
ここが設計上いちばん危ない場所です。Price IDとプラン名の対応表をコードに持って判定する実装は素直で分かりやすいのですが、対応表に載っていないIDが来たときの振る舞いを決めていないと、値上げした日に事故ります。載っていないIDを機械的に無料プラン扱いにする実装だと、新Priceに切り替えた契約者が全員無料に落ちます。
対応表を持つこと自体は問題ありません。決めておくべきは「知らないIDが来たらどうするか」です。安全側に倒すなら、知らないIDのときは現在のプランを変更しないのが基本になります。無料に落とすのも有料に格上げするのも、どちらも実害が出ます。
プラン数は少ないほど運用が軽い
プランを増やすと、アップグレード・ダウングレードの組み合わせが増え、日割りの検証パターンも増えます。個人開発なら、無料+有料1〜2段が扱いやすい範囲です。カスタマーポータルでプラン変更をさせる場合、選択肢として提示できるのは最大10商品までという上限もあります(Stripe公式ドキュメント「顧客にカスタマーポータルを提供する」)。
Checkout Sessionは入口でしかない
Checkout SessionはStripeがホストする決済ページです。カード情報が自分のサーバーを通らないため、実装の負担もセキュリティ上の責任範囲も小さくできます。サブスクリプションなら mode を subscription にしてセッションを作り、返ってきたURLへ利用者を飛ばすだけです。
問題は、そのあとに戻ってくる success_url の扱いです。決済完了ページに到達したことは、支払いが成立した証明にはなりません。利用者は決済後にブラウザを閉じることがありますし、逆にURLを直接叩くこともできます。戻り先の画面で「ありがとうございます」と表示するのは構いませんが、そこでプランを書き換えてはいけません。
プランを書き換える権限を持つのはWebhookだけ、と決めてしまうのが結局いちばん堅くなります。戻り先の画面は「処理中です」と伝えるだけにして、実際のデータ更新はWebhookの到着で行う。この分担にしておくと、あとで支払い方法を増やしたときにも同じ形が使えます。
Checkout Sessionを作る具体的なコードと、Webhookの署名検証・冪等性の実装はNext.js×Cloudflare Workers×D1でSaaS開発する方法にまとめてあります。この記事はそこには踏み込まず、受け取ったあとの設計だけを扱います。
解約と支払い方法の変更は自作しない
課金まわりで自作すると割に合わないのが、解約画面・支払い方法の変更画面・請求書の一覧です。これらはStripeのカスタマーポータルが最初から持っています。ポータルに寄せると、自分で書くコードはセッションを1つ作って発行されたURLへ飛ばすだけになります。
ポータルに任せられること
- 支払い方法の更新(カードの差し替え)
- プランの変更
- 解約(即時か、現在の請求期間の終了時かを選べる)
- 納税者番号を含む請求先情報の更新
- 現在および過去の請求書の支払い・ダウンロード・表示
解約を引き止めるためのクーポン提示や、解約理由の収集も設定でまかなえます。理由はWebhook経由で受け取れるので、自前のアンケート画面を作る必要もありません。
先に知っておくべき制約
便利な代わりに、設計を縛ってくる制約がいくつかあります。あとから気づくと画面構成をやり直すことになるので、先に把握しておきます。
| 制約 | 設計への影響 |
|---|---|
| ポータルのセッションは作成から5分で期限切れ(使い始めた場合は最終操作から1時間) | URLを事前に発行して保存しておく設計にはできない。押された瞬間に作る |
| iframe内に表示できない | 自サイトに埋め込む形にはできない。別画面へ遷移させる |
| 複数商品・従量課金・請求書送付の契約は、解約はできるが変更はできない | プラン変更を提供するなら、契約は単一商品の定額に寄せる |
| プラン変更の選択肢は最大10商品 | プランを増やしすぎない |
ポータルへ送り出す処理は、契約中の利用者かどうかを確かめてからセッションを作る、という形になります。
// 顧客IDを持っていない=一度も課金していない利用者はポータルに入れない
if (!user.stripeCustomerId) {
return { error: "契約が見つかりません" }
}
// セッションは押された瞬間に作る(5分で失効するため事前生成しない)
const session = await stripe.billingPortal.sessions.create({
customer: user.stripeCustomerId,
return_url: `${baseUrl}/dashboard`,
})
return { url: session.url }ここで重要なのは、ポータルでの操作結果はこのコードには返ってこないことです。利用者がポータルでプランを変えても解約しても、アプリが知る手段はWebhookしかありません。次章がその設計になります。
拾うべきWebhookは3つ|イベント別の対応表
Stripeが送ってくるイベントは膨大ですが、定額のサブスクリプションを回すだけなら中心は3つです。公式も「実装で必要なイベントのタイプのみを受信するように設定します。その他のイベント(またはすべてのイベント)をリッスンすると、お客様のサーバーに過度の負荷がかかるため、お勧めしません」としています(Stripe公式ドキュメント「Webhook エンドポイントで Stripe イベントを受信する」)。
| イベント | いつ来るか | やること |
|---|---|---|
checkout.session.completed | 決済画面での初回契約が成立したとき | 顧客IDと契約IDを保存し、プランを付与する |
customer.subscription.updated | プラン変更・状態変化・解約予約など、契約が更新されたとき | プランと契約状態の両方を同期する |
customer.subscription.deleted | 契約が終了したとき | 無料プランに戻し、契約IDを外す |
メール通知まで自分でやるなら、次の2つを足します。プラン判定には使いません。
invoice.payment_failed— 請求が失敗したとき。attempt_countにそれまでの試行回数が入るcustomer.subscription.trial_will_end— トライアル終了の3日前(残り3日未満で開始した場合は即座に発火する)
前提にしてはいけないこと
設計の前に、Stripe公式が明言している2つの性質を押さえます。どちらも「そうなっていてほしい」と思い込みやすい部分です。
- 順序は保証されない。公式の記述は「Stripe は、イベントが生成された順序で配信されることを保証しません」。あとから発生した変更が先に届くことがある
- 同じイベントが複数回届きうる。公式の記述は「Webhook エンドポイントは、同じイベントを複数回受信する可能性があります」。処理済みのイベントIDを記録して弾く
重複より順序のほうが厄介です。重複は「同じ更新をもう一度やるだけ」で済むことが多いのに対し、順序の入れ替わりは古い状態で新しい状態を上書きするため、結果が静かに壊れます。契約の情報を丸ごと受け取って上書きするのではなく、届いたイベントのIDを記録しつつ、必要なら契約IDから最新の状態をAPIで取り直すほうが安全です。
あわせて、応答は速く返します。公式は「タイムアウトを引き起こす可能性のある複雑なロジックの前に、成功ステータスコード(2xx)をすばやく返します」としています。メール送信のような重い処理を同期で挟むと、応答が遅れてタイムアウト扱いになり、リトライを呼び込みます。
初回契約のイベントだけだと、プラン変更が丸ごと漏れる
いちばん多い作り落としが、checkout.session.completed だけを処理して終わらせてしまう形です。初回の契約は正しく通るので、テストでは問題なく動いているように見えます。
ところが、利用者がカスタマーポータルでプランを変えたとき、決済画面は経由しません。したがって checkout.session.completed は発生しません。届くのは customer.subscription.updated です。これを拾っていなければ、下位プランに落とした利用者が上位プランの機能を使い続けます。逆に上位へ上げた利用者は、支払っているのに機能が開放されません。
もう一つ、customer.subscription.updated の実装で見落とされやすいのが契約状態(status)を見ていないことです。Price IDだけを見てプラン名を決めていると、支払いが失敗して契約が滞納状態になってもPrice IDは変わらないため、有料プランのまま据え置かれます。
Price IDと状態の両方を見て、知らないIDが来たら触らない。この3点を守った形が下のコードです。
// アクセスを与えてよい契約状態だけを列挙する
const ACTIVE_STATUSES = new Set(["active", "trialing"])
// 猶予を与える状態。ここでは機能を止めない
const GRACE_STATUSES = new Set(["past_due"])
function resolvePlan(
status: string,
priceId: string,
currentPlan: string
): string {
// 支払いを尽くして打ち切られた状態は、必ず無料に戻す
if (status === "canceled" || status === "unpaid" || status === "incomplete_expired") {
return "free"
}
// 滞納中は猶予期間。プランは据え置いて、通知だけ出す
if (GRACE_STATUSES.has(status)) {
return currentPlan
}
if (!ACTIVE_STATUSES.has(status)) {
return currentPlan
}
const plan = PLAN_BY_PRICE[priceId]
// 知らないPrice IDは「値上げで新しく作ったPrice」の可能性がある。
// 無料に落とすと契約者を締め出すため、現状維持で運用者に気づかせる
if (!plan) {
console.error("未登録のPrice IDです", { priceId, status })
return currentPlan
}
return plan
}知らないIDのときにログを残しているのが要点です。黙って現状維持にすると気づけないので、運用者が検知できる形にしておきます。
支払いが失敗したとき、いつ機能を止めるか
カードの期限切れや残高不足は、一定の割合で必ず起きます。ここで即座に機能を止めると、払う気のある利用者まで失います。かといって放置すれば無料で使われ続けます。Stripeはこの猶予期間の設計をダッシュボード側に持っています。
リトライの既定値と、その後の行き先
失敗した支払いは自動で再試行されます。Smart Retriesの推奨される既定設定は2週間以内に8回で、期間は1週間・2週間・3週間・1か月・2か月から選べます。自前のスケジュールに切り替える場合は、前回から日数を空けて最大3回までです(Stripe公式ドキュメント「支払いの再試行を自動化する」)。
再試行を尽くしたあと契約をどうするかは、ダッシュボードで3択から選びます。この選択が、アプリ側の分岐と対になっていないと事故ります。
| 設定 | 最終的な状態 | アプリ側の扱い |
|---|---|---|
| サブスクリプションをキャンセルする | canceled | 無料に戻す。customer.subscription.deleted も届く |
| 未払いとしてマーク | unpaid | アクセスを取り消す。契約自体は残る |
| 期日超過のままにする | past_due のまま | 自分で打ち切り期限を決めて止める必要がある |
unpaid の扱いについて、公式は明確に指示しています。「サブスクリプションが unpaid の場合は、past_due の時点で支払いの試行と再試行がすでに行われているため、プロダクトへのアクセスを取り消します」(Stripe公式ドキュメント「サブスクリプションの仕組み」)。迷ったらこの指示に従うのが無難です。
再試行されない失敗がある
すべての失敗が再試行されるわけではありません。カード発行会社がハードデクラインを返した場合、Stripeは自動での再試行を行いません。該当するコードには incorrect_number、lost_card、stolen_card、authentication_required、transaction_not_allowed などがあります。
紛らわしいのは、この場合も再試行はスケジュールされ続け、attempt_count も増えていく点です。実際の決済は新しい支払い方法が登録されるまで行われません。つまり「リトライ回数が増えているから待っていればいい」という判断は成り立ちません。カードの差し替えを促す通知を出さないかぎり、状況は変わらないままです。
プラン変更と日割りの、直感に反する挙動
期間の途中でプランを変えると日割りが発生します。Stripeは既定で秒単位に計算し、未使用分をマイナス、新しい料金の残り期間分をプラスとして積みます。月額1,000円から2,000円へ期間の半ばで変えたなら、マイナス500円とプラス1,000円で差し引き500円という形です。
ここで誤解しやすいのが、その差額がいつ動くかです。公式の記述はこうです。「マイナスの比例配分は自動的に返金されず、プラスの比例配分はすぐには請求されません」(Stripe公式ドキュメント「比例配分」)。アップグレードしても即座に課金されるわけではなく、ダウングレードしても即座に返金されるわけでもありません。どちらも次回の請求書に載ります。
この挙動は proration_behavior で切り替えます。既定値は create_prorations です。
| 値 | 挙動 | 向いている場面 |
|---|---|---|
create_prorations(既定) | 日割り項目を作り、次回の請求書に載せる | 基本はこれ |
always_invoice | 日割りを計算し、その場で請求書を発行して請求する | アップグレードを即時に課金したいとき |
none | 日割りを作らない。次回から新しい料金の全額 | ダウングレードを次の期間から効かせたいとき |
注意点として、今後の日割りをまとめて無効にする設定は存在しません。公式は「サブスクリプションの今後の比例配分をすべて無効にするパラメータはありません」と明記しています。無効にしたいなら、日割りが発生しうる全リクエストで毎回 none を渡す必要があります。
金額を利用者に見せてから確定したい場合は、請求書のプレビューを先に取ります。ただし秒単位で計算されるため、プレビューから確定までの間に金額が動きます。ずれを防ぐには、プレビュー時に使った日付を更新時にも同じ値で渡します。
// 秒単位で計算されるため、プレビューと確定で同じ基準時刻を使う
const prorationDate = Math.floor(Date.now() / 1000)
// 1. 先に金額を見せる
const preview = await stripe.invoices.createPreview({
customer: customerId,
subscription: subscriptionId,
subscription_details: {
items: [{ id: itemId, price: newPriceId }],
proration_date: prorationDate,
},
})
// 2. 利用者が同意したら、同じ prorationDate で確定する
await stripe.subscriptions.update(subscriptionId, {
items: [{ id: itemId, price: newPriceId }],
proration_date: prorationDate,
})無料トライアルを付けるかどうかの判断
トライアルの設計は「カード登録を求めるかどうか」でほぼ決まります。求めない場合、トライアル終了時に支払い方法がない契約をどう扱うかを、あらかじめ指定しておく必要があります。
| 終了時の指定 | 契約はどうなるか | 届くイベント |
|---|---|---|
cancel | canceled になって終了する | customer.subscription.deleted |
pause | paused で止まり、請求書が作られなくなる | customer.subscription.paused |
paused は、支払い方法が登録されて明示的に再開されるまでその状態を保ちます。再開時には customer.subscription.resumed が届くので、機能を戻す処理をここに書きます。この2つのイベントを実装していないと、カード登録なしトライアルは終了時に何も起きないまま放置されます。
個人開発で運用の手間を最小にしたいなら、カード登録ありのトライアルにして、終了3日前に届く customer.subscription.trial_will_end で案内メールを出す形が扱いやすくなります。扱うイベントが1つ増えるだけで、paused と resumed の分岐を持たずに済みます。
そもそも課金を載せるほどのプロダクトになっているか迷う段階なら、個人開発の進め方そのものを整理したインディーメーカーとは?個人でプロダクトを作る新しい働き方も参考になります。
【検証】本番へ切り替える前に確認する5項目
テストモードで通っていても、本番に切り替えた瞬間に落ちる箇所はだいたい決まっています。以下を順に確認します。合否ラインを併記しているので、そのまま手順として使えます。
- Webhookの署名シークレットが本番用になっているか。テストと本番でエンドポイントを分ければシークレットも別になる。合格=本番のエンドポイントにイベントを送って
2xxが返ること。テスト用のまま残っていると、すべての署名検証が落ちて一律400になる - Price IDが本番モードのものか。テストモードで作ったPriceは本番には存在しない。合格=本番の決済画面が価格つきで開くこと
- 同じイベントを2回送って、結果が1回分か。ダッシュボードから同一イベントを再送する。合格=プラン付与やレコード追加が二重に起きないこと
- ポータルでのプラン変更がアプリに反映されるか。カスタマーポータルからプランを変更する。合格=自分のデータベースのプランが数秒以内に切り替わること。変わらなければ
customer.subscription.updatedを拾えていない - 支払い失敗が意図した状態に落ちるか。テストカードで失敗させる。合格=ダッシュボードで選んだ設定どおりの状態に遷移し、アプリ側の分岐がそれに対応していること
5番目に使うテストカードは、目的によって使い分けます。有効期限は将来の日付、セキュリティコードは任意の3桁で通ります。
| カード番号 | 結果 | 自動リトライ |
|---|---|---|
| 4242 4242 4242 4242 | 成功 | — |
| 4000 0000 0000 9995 | 資金不足で拒否 | される |
| 4000 0000 0000 9987 | 紛失カードとして拒否 | されない |
| 4000 0000 0000 9979 | 盗難カードとして拒否 | されない |
資金不足の番号でリトライの流れを、紛失・盗難の番号で「リトライされずに止まる」流れを確認できます(Stripe公式ドキュメント「テスト」)。後者を試しておかないと、カード差し替えの案内が必要な状況に気づけません。
不合格だったときの切り分け
前章で落ちた場合、症状から原因を絞り込めます。ここで挙げた次の一手までを1周とし、それ以上は個別に追ってください。
| 症状 | 疑うべき原因 | 次の一手 |
|---|---|---|
| 署名検証が必ず失敗する | フレームワークがボディをパース済みで、生の本文が届いていない | そのルートだけ生の文字列で受け取る |
| 本番だけイベントが届かない | エンドポイントが 3xx リダイレクトを返している。リダイレクトは失敗扱い | リダイレクト後のURLを直接登録する |
| ときどきタイムアウトする | 応答前に重い処理を同期で実行している | 先に 2xx を返し、処理は非同期に逃がす |
| 署名は合うのに古いと言われる | サーバーの時刻がずれている(許容差は既定5分) | 時刻同期を設定する。許容差を 0 にはしない |
| プランが変わらない | 初回契約のイベントしか処理していない | customer.subscription.updated を追加する |
配信が失敗したイベントは取り返せます。自動リトライは本番で指数バックオフのまま最長3日間続きます。手動での再送はダッシュボードから15日以内、Stripe CLIからは30日以内まで可能です。原因を直したうえで再送すれば、落としたイベントを回収できます。
ただし手動で再送しても自動リトライは止まりません。公式は「以前に配信に失敗したイベントを Webhook エンドポイントに手動で再送信した場合、その結果のステータスコードが 2xx になったとしても、Stripe の自動再試行動作は解除されません」としています。二重処理を弾く仕組みがここでも効いてきます。
技術の外側でそろえるもの
継続課金を日本国内の利用者に提供する場合、実装とは別に用意するものがあります。要件は事業形態や取扱内容で変わるため、ここでは確認先だけを示します。判断は必ず一次情報と専門家に当たってください。
- 特定商取引法に基づく表記 — 販売事業者名・連絡先・支払時期・解約条件など。要件は消費者庁の特定商取引法ガイドで確認する
- 利用規約 — 解約時に日割り返金をするかしないかを明記しておく。前章のとおり、日割りは既定では自動返金されない
- 適格請求書(インボイス)の扱い — 法人利用者から求められる場面がある。制度の内容は国税庁のインボイス制度特設サイトを参照する
決済手数料の水準は、プラットフォームやストアによって差があります。アプリストアを経由する場合の考え方はApple Small Business Programとは|手数料15%の仕組みに、モールや既製カートを含めた比較はECサイト構築の費用比較|月商から選ぶモール出店・ASP・自社ECにまとめています。自前で決済を持つかどうかを迷っている段階なら、後者から見るほうが判断が早くなります。
まとめ
Stripeのサブスクリプション課金は、決済画面を出すところまでは短時間で組めます。運用で壊れるのはそのあとで、原因はほぼ「契約が動いたことをアプリが知らない」ことに集約されます。
- プランを書き換える権限はWebhookだけに持たせる。戻り先の画面では書き換えない
- 拾うのは初回契約・契約更新・契約終了の3イベント。契約更新はPrice IDと状態の両方を見る
- 知らないPrice IDが来たらプランを変えず、ログに残す。値上げの日に効いてくる
- 解約・支払い方法の変更・請求書はカスタマーポータルに寄せる
- 本番切り替え前に、シークレット・Price ID・二重処理・プラン変更の反映・支払い失敗の5項目を確認する
次の一手は、5項目の検証で落ちた箇所を1つだけ直すことです。すべてを一度に整えようとせず、プラン変更が反映されるところまでを先に通してください。そこが通れば、残りは同じ形の繰り返しになります。
課金を組み込んだサービスを検索から見つけてもらう段階まで来たら、サイト側の状態も確認しておくと手戻りが減ります。
決済まわりの設計や実装を相談したい場合は、制作の依頼も受け付けています。
よくある質問(FAQ)
Q. 決済完了ページに戻ってきた時点でプランを付与してはいけませんか?
やめたほうが安全です。戻り先のURLは利用者のブラウザが遷移するだけのもので、支払いの成立を保証しません。決済後にブラウザを閉じれば到達しませんし、URLを直接開くこともできます。戻り先の画面は状態の表示にとどめ、プランの書き換えは checkout.session.completed の受信で行ってください。表示上のずれが気になる場合は、その画面で契約状態をAPIから読み直して表示すれば、書き換え権限を渡さずに解決できます。
Q. 同じイベントが2回届くことはありますか?
あります。Stripe公式が「Webhook エンドポイントは、同じイベントを複数回受信する可能性があります」と明記しています。対策は、処理したイベントIDを保存しておき、すでに記録済みなら何もせずに 2xx を返すことです。記録は処理の前に行います。あわせて、イベントの配信順序も保証されていない点に注意してください。重複より順序の入れ替わりのほうが影響が大きく、古い内容で新しい状態を上書きすると気づきにくい不整合になります。
Q. ダウングレードを次回の請求日から適用するにはどうしますか?
契約を更新するときに proration_behavior を none に指定します。日割りの調整が作られなくなり、次回の請求から新しい料金の全額が請求されます。既定値は create_prorations なので、指定しなければ日割りが作られます。なお、今後の日割りをまとめて止める設定は用意されていません。日割りが発生しうるリクエストごとに毎回指定する必要があります。
Q. 支払いに失敗した利用者の機能は、いつ止めるべきですか?
再試行を尽くしたあとの状態で判断します。Stripe公式は unpaid について「past_due の時点で支払いの試行と再試行がすでに行われているため、プロダクトへのアクセスを取り消します」としています。再試行中にあたる past_due の間は猶予期間として機能を維持し、通知でカードの更新を促すのが一般的です。ただし打ち切り後の状態を past_due のままにする設定を選んだ場合、Stripe側では状態が変わらないため、自分で期限を決めて止める処理が必要になります。
Q. 解約画面は自分で作る必要がありますか?
必要ありません。カスタマーポータルが解約に対応しており、即時解約と現在の請求期間の終了時の解約を選べます。引き止めのクーポン提示や解約理由の収集も設定でまかなえ、理由はWebhookで受け取れます。自分で書くのは、ポータルのセッションを作って発行されたURLへ遷移させる処理だけです。ただしセッションは作成から5分で期限切れになるため、URLを事前に発行して保存しておく設計にはできません。押された時点で作ってください。
Q. 落としてしまったWebhookイベントは取り戻せますか?
取り戻せます。本番環境では指数バックオフで最長3日間、自動的に再試行され続けます。それも過ぎた場合、ダッシュボードからの再送はイベント作成後15日以内、Stripe CLIの再送コマンドは30日以内まで機能します。原因を直してから再送すれば回収できます。注意点として、手動で再送しても自動再試行は解除されません。二重に処理されないよう、イベントIDでの重複排除を先に入れておいてください。
