READ
~/eforce-tec/blog/posts/stripe-subscription-webhook-design.md
markdown
3 min

Stripe でサブスクリプションを作る前に決めておく4つのこと

date2026-08-19author難波 優毅(Masataka Namba)techstripephp決済設計3 min

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か月目で全員が切れる、という事故になる。継続課金の成功を受け取って期限を延ばすのは必須だ。

NOTE

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. 支払いが止まったとき、何を止めるか

技術というより方針の話だが、実装前に決めておかないと後で揉める。

支払い失敗の瞬間に機能を全部止めると、利用者からは「アップデートで壊れた」ように見える。実際は請求の問題なのに、問い合わせ対応のコストがこちらに来る。

現実的には次のような段階を置くことが多い。

  1. past_due になった時点では止めない。画面上に支払い方法の更新を促す表示を出す
  2. Stripe のリトライ(既定で数回・数週間)が尽きて canceled になったら、中心的な機能ひとつだけを止める
  3. 止まった場所には、状況の説明と再開の導線を必ず置く

「サービスが壊れていない」「契約が切れていることが伝わる」「再開できる」の3つを同時に満たすのが目標になる。ここを丁寧にやると、解約後に戻ってくる人の割合が変わる。

まとめ

課金の実装そのものは Stripe がほとんど面倒を見てくれる。設計で差がつくのは、状態をどこに持つか・いつ更新するか・止まったときにどう見えるかの3点だ。この記事の4項目を先に決めておけば、あとの実装は素直に進む。

$contact --subject "相談"

こうした実装を、仕事としてお引き受けしています。

記事で書いているような設計・実装を、受託でも行っています。Webサイトの制作から、業務の仕組み化、決済まわりの実装まで。要件が固まっていない段階のご相談で構いません。