Stripe で月額課金を実装するだけなら、公式ドキュメントの通りに進めれば数時間で動く。難しいのはそこではなく、「いま誰が有効な契約者なのか」をアプリケーション側でどう持つかのほうだ。
ここを決めずに作り始めると、あとから「解約したはずの人が使えている」「支払いが失敗したのに止まらない」といった不具合が出て、そのたびに Stripe の管理画面と自前の DB を突き合わせることになる。実装前に決めておきたい論点を4つ挙げる。
1. 契約状態は、自分の DB に持つ
最初に迷うのがここだ。「利用可否を判定するたびに Stripe の API に問い合わせればいい」という設計は、コード量が一番少なくて済む。
ただしこれをやると、外部 API の応答時間が、そのままこちらのページ表示速度になる。Stripe が遅い日はサービスも遅い。障害が起きれば全ユーザーが使えなくなる。問い合わせ回数も利用者数に比例して増えていく。
契約状態は自前のテーブルに持ち、Stripe からの通知で更新する。判定は自分の DB を読むだけにする。これが原則になる。
users
id, email, ...
subscriptions
user_id
stripe_customer_id
stripe_subscription_id
status -- active / past_due / canceled
current_period_end -- ここまでは使える、という日時
判定ロジックは「status が active で、かつ current_period_end が未来か」だけになる。シンプルで速い。
2. Webhook で受けるイベントを決める
状態を更新するトリガーが Webhook だ。最低限、次の4つを受ければ回る。
| イベント | やること |
|---|---|
checkout.session.completed |
契約レコードを作る |
invoice.paid |
current_period_end を延ばす |
customer.subscription.updated |
status を同期(支払い失敗・プラン変更) |
customer.subscription.deleted |
解約として閉じる |
invoice.paid を拾っていないと、初月は動くのに2か月目で全員が切れる、という事故になる。継続課金の成功を受け取って期限を延ばすのは必須だ。
Webhook のエンドポイントでは、必ず署名を検証すること。Stripe が付ける Stripe-Signature ヘッダと署名シークレットを突き合わせる処理は公式ライブラリに用意されている。これを省くと、URL さえ知っていれば誰でも「課金が成功した」という偽の通知を送れてしまう。
3. 同じ通知が2回来る前提で書く
Webhook は同じイベントが複数回届くことがある。ネットワークの再送や、こちらが 200 を返す前にタイムアウトした場合などだ。
対策は難しくない。受け取ったイベント ID を保存しておき、処理済みなら何もせず 200 を返す。
// イベントIDを一意キーにして、二重処理を防ぐ
$stmt = $db->prepare('INSERT IGNORE INTO webhook_events (event_id) VALUES (?)');
$stmt->execute([$event->id]);
if ($stmt->rowCount() === 0) {
http_response_code(200); // すでに処理済み
exit;
}
これを入れておかないと、invoice.paid が2回届いたときに期限が2か月分伸びる、といった地味な事故が起きる。
4. 支払いが止まったとき、何を止めるか
技術というより方針の話だが、実装前に決めておかないと後で揉める。
支払い失敗の瞬間に機能を全部止めると、利用者からは「アップデートで壊れた」ように見える。実際は請求の問題なのに、問い合わせ対応のコストがこちらに来る。
現実的には次のような段階を置くことが多い。
past_dueになった時点では止めない。画面上に支払い方法の更新を促す表示を出す- Stripe のリトライ(既定で数回・数週間)が尽きて
canceledになったら、中心的な機能ひとつだけを止める - 止まった場所には、状況の説明と再開の導線を必ず置く
「サービスが壊れていない」「契約が切れていることが伝わる」「再開できる」の3つを同時に満たすのが目標になる。ここを丁寧にやると、解約後に戻ってくる人の割合が変わる。
まとめ
課金の実装そのものは Stripe がほとんど面倒を見てくれる。設計で差がつくのは、状態をどこに持つか・いつ更新するか・止まったときにどう見えるかの3点だ。この記事の4項目を先に決めておけば、あとの実装は素直に進む。