Blog
Stripeのサブスクで支払い失敗後の再請求を自動化する
サブスクリプション請求管理において、請求管理とは実装時に見落とされがちなOpsの1つです。「支払いが失敗しました」という通知を受け取り、ダッシュボードを開く。該当するサブスクリプションを探してインボイスのページに移動し、「支払いページ」のリンクをコピー。そしてメールクライアントを開いて、定型文をペーストし、リンクを埋め込んで送信。このような作業を月に何回も繰り返すことになります。
もし、この一連の流れが自動で回るとしたらどうでしょうか?この記事では、StripeのSmart Retriesで自動再試行を最適化し、それでも失敗した場合にWebhookで再請求リンクを自動送信する実装パターンを紹介します。
請求管理・未払いの回収がなぜ重要か
解約されるサブスクリプションの2割近くは支払い失敗が原因である可能性があります。これはinvoluntary churn(意図しない解約)と呼ばれており、顧客は解約する意図がないのに、カードの有効期限切れや一時的な残高不足によって解約されてしまっている状況を指します。
製品に不満があるわけでも、価格が高いと感じているわけでもありません。単に、クレジットカードの有効期限が切れていた、口座の残高が一時的に不足していた、銀行のエラーが発生した、といった理由です。つまりは、意図しない未払いを正しく回収・リカバリーできれば、それだけで解約率を改善できるということです。
従来、支払い失敗への対応は「固定間隔での再試行」でした。3日後、7日後、14日後に自動で課金を試みる、といった単純なルールです。しかしこの方法には問題があります。給料日が月末の顧客に対して月初に再試行しても成功しません。深夜3時の課金は不正検知システムに引っかかりやすく、成功率が下がります。
Stripeの機械学習ベースで行われる再請求 – Smart Retries
Stripeの場合、機械学習アルゴリズムを使って「いつ再試行すれば成功しやすいか」を判定するSmart Retries機能が用意されています。Stripeネットワーク全体の数十億件の取引データから、最適なタイミングを導き出します。デフォルト設定は「2週間で8回の再試行」で、1週間から2ヶ月のサイクルを選び、その期間内での最大試行回数を指定できます。Smart Retriesが有効な場合、この回数と期間の制約内で、機械学習によって「今がベストタイミング」と判断されたときに自動で課金を試みます。
それでも再試行がすべて失敗した場合、顧客に「別のカードで支払ってください」と依頼する必要があります。
リトライ完了後の手動再請求
すべての再試行が失敗したインボイスには、Hosted Invoice Page の URL が発行されています。この URL を顧客に送れば、ブラウザで支払いページを開いて、別のカードで決済できます。
ダッシュボードから手動で送る場合、以下の手順です。
- ダッシュボードの「Billing > インボイス」から該当のインボイスを開く
- 「詳細」欄の「支払いページ」リンクをコピー
- 顧客にメールで送信
この URL は、インボイスオブジェクトの hosted_invoice_url プロパティに格納されています。API経由で取得する場合は以下のようになります。
const stripe = require('stripe')('sk_test_...');
const invoice = await stripe.invoices.retrieve('in_...');
console.log(invoice.hosted_invoice_url);
// => "https://invoice.stripe.com/i/acct_.../test_..."
顧客がこのURLを開くと、Stripeがホストする支払いページが表示され、カード情報を入力すればその場で決済が完了します。
そしてAPIで処理できるということは、Webhook を使うことで自動化も可能ということです。
Webhook で再請求を自動化する
Stripe は、支払いが失敗したタイミングで invoice.payment_failed イベントを送信します。このイベントには、失敗したインボイスオブジェクトが含まれており、hosted_invoice_url も取得できます。
Webhook エンドポイントを実装し、このイベントを受け取ったら自動でメールを送信すれば、手動作業はゼロになります。
以下は Node.js + Express での実装例です。
const express = require('express');
const stripe = require('stripe')('sk_test_...');
const nodemailer = require('nodemailer');
const app = express();
// Webhookのraw bodyが必要なため、express.json()は使わない
app.post('/webhook', express.raw({type: 'application/json'}), async (req, res) => {
const sig = req.headers['stripe-signature'];
const endpointSecret = 'whsec_...';
let event;
try {
event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
} catch (err) {
console.log(`Webhook signature verification failed: ${err.message}`);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// invoice.payment_failed イベントのみ処理
if (event.type === 'invoice.payment_failed') {
const invoice = event.data.object;
const hostedUrl = invoice.hosted_invoice_url;
const customerEmail = invoice.customer_email;
// メール送信処理(例: nodemailer)
const transporter = nodemailer.createTransport({/* 設定 */});
await transporter.sendMail({
to: customerEmail,
subject: 'お支払いが失敗しました',
html: `
<p>お支払いの処理に失敗しました。</p>
<p>別のお支払い方法をご利用いただく場合は、以下のリンクからお手続きください。</p>
<a href="${hostedUrl}">お支払いページへ</a>
`
});
console.log(`Payment failed email sent to ${customerEmail}`);
}
res.status(200).json({received: true});
});
app.listen(3000, () => console.log('Webhook server running on port 3000'));
この実装により、支払いが失敗した瞬間に、顧客へ自動でメールが送信されます。ダッシュボードを開く必要も、リンクをコピーする必要もありません。
Webhook エンドポイントの登録は、ダッシュボードの Developers > Webhooks から行います。リッスンするイベントとして invoice.payment_failed を選択し、エンドポイントの URL を指定すれば完了です。
MLベースの再請求と、システムによる通知で意図しない解約を予防する
Smart Retries と Webhook 自動化を組み合わせることで、支払い失敗への対応が完全に自動化されます。
- Smart Retries で自動回収を最大化する: 2週間で8回、MLが最適なタイミングを判断して再試行します
- すべての再試行が失敗したら、Webhook で即座に通知:
invoice.payment_failedイベントをリスンし、hosted_invoice_urlを含むメールを自動送信
これで、involuntary churn による売上損失を最小限に抑えつつ、運用コストもゼロにできます。支払い失敗の通知が来ても、もう焦る必要はありません。すべて自動で回っています。
Tools to Support Stripe Development
We provide helpful tools to extend the Stripe Dashboard and streamline development and testing.
View All ToolsRelated Articles
Support This Project
If you find this content helpful, consider supporting the project through GitHub Sponsors. Your support helps maintain and improve these tools.
