メインコンテンツへスキップ
友田 陽大
AIで作ったアプリの運営
バイブコーディング
決済
セキュリティ
個人開発
信頼性

AIに決済を作らせたとき、二重課金が起きる理由と防ぎ方

AIが書いた決済処理で顧客が二重に課金される仕組みを、非エンジニア向けに解説します。Webhookが複数回届くのは仕様であること、冪等性という対策、金額をブラウザから送ってはいけない理由、署名検証の必要性。本番二重課金0件を維持した決済基盤の実装知見に基づきます。

公開日
読了時間
10分
著者
友田 陽大
シェア

お金が絡むと、間違いのコストが変わります。データが見えてしまうのも深刻ですが、二重に課金された顧客は、その場で怒って、二度と戻ってきません。返金はできても、信用は戻りません。

そして厄介なことに、この不具合は手元のテストでは絶対に再現しません。本番で、しかも一番忙しい時間帯に初めて出ます。

決済機能が無いアプリの方は、この記事は飛ばして構いません。


1. まず、何が起きるのか

Stripe などの決済サービスを使うと、こういう流れになります。

  1. 顧客が「購入」を押す
  2. 決済サービスの画面でカード情報を入力し、支払いが完了する
  3. 決済サービスがあなたのアプリに「支払われましたよ」と通知を送る(Webhook と呼びます)
  4. あなたのアプリがその通知を受けて、商品を渡す・残高を増やす・注文を確定する

問題は 3 です。

この通知は、同じ内容で複数回届くことがあります。

そして 4 の処理が「通知を受けるたびに残高を足す」形だと——2回届けば2回足されます。それが二重付与です。


2. なぜ複数回届くのか — これは仕様です

「通知が2回来るなんて、バグでは?」と思うかもしれません。違います。設計です。

決済サービスにとって最悪の事態は、通知が届かないことです。支払いは完了したのに商品が渡されない——これは決済サービスの信頼を根本から損ないます。

だから彼らはこう設計しています。

確実に1回届けるより、最低1回は届ける

これを at-least-once(最低1回)配信 と呼びます。次のような場合に再送が起きます。

  • あなたのアプリの応答が遅かった(タイムアウト扱いになる)
  • あなたのアプリが一時的にエラーを返した
  • ネットワークが不安定だった
  • 決済サービス側の内部的な再試行

Stripe の公式ドキュメントにも、イベントが複数回配信され得ることと、受け取る側が冪等に処理すべきことが明記されています。

つまり——二重に届けないのは決済サービスの仕事ではなく、二重に処理しないのがあなたの仕事です。契約としてそうなっています。


3. なぜAIは書き漏らすのか

「支払いが完了したら残高を増やして」と頼めば、AI はその通りに書きます。それは正しい実装です。

ただし「同じ通知が2回来たらどうするか」は、頼まれていないので考慮されません。

これは意地悪でも手抜きでもなく、指示の範囲外だからです。そして——

手元での動作確認では、通知は1回しか来ません。

自分でテスト購入すると、通知は1回。正常に動く。何度やっても正常に動く。二重処理の分岐は一度も実行されないからです。

Veracode の検証でAI生成コードの45%に脆弱性が見つかったという結果がありますが、この種の「エラーは出ないが本番でだけ壊れる」欠陥は、静的な検査でも捕まえにくい部類です。設計として最初から入れておくしかありません。


4. 対策 — 冪等性という考え方

対策には名前があります。冪等性(べきとうせい、idempotency)

何回やっても結果が同じになる性質

エレベーターのボタンを5回押しても、エレベーターは1回分しか来ません。これが冪等です。押した回数だけエレベーターが来たら困ります。

決済処理でこれを実現する方法は、実はとても単純です。

やること

  1. 決済サービスからの通知には、イベントIDevt_ で始まる一意の文字列など)が含まれている
  2. 処理する前に、そのIDがすでに処理済みかどうかをデータベースで確認する
  3. 処理済みなら、何もせずに「受け取りました」と応答する
  4. 未処理なら、処理してからIDを記録する

これだけです。難しくありません。忘れられるだけです。

実装上の要点3つ

(1)記録と業務処理は同じトランザクションにまとめる

「残高を増やす」と「IDを記録する」を別々に行うと、その間でアプリが落ちたときに、残高は増えたのにIDが記録されていない状態になります。次の通知でまた増えます。この2つは必ず一括で成功か失敗かにします。

(2)IDの列に「一意制約」を付ける

「確認してから書き込む」の間に、もう1つの通知が同時に入ることがあります(競合状態)。データベース側で「同じIDは2行入れられない」という制約を付けておけば、後から来たほうが確実に弾かれます。アプリ側のチェックだけに頼らない、というのが要点です。

(3)返すステータスコードを間違えない

処理済みで何もしなかった場合も、成功(200)を返します。エラーを返すと、決済サービスは「まだ届いていない」と判断してさらに再送します。

逆に、本当に処理に失敗したときはエラーを返します。ここで成功を返してしまうと、決済サービスは「届いた」と判断して再送をやめ、通知が永久に失われます。

実案件での確認

私が担当した決済基盤では、この設計により本番での二重課金を0件に保っています。4つの面(顧客・加盟店・管理・店頭端末)を横断する規模の決済プラットフォームでの実績です。

強調しておきたいのは、これが技術的に高度な話ではないことです。「必ずやる」と決めているかどうかの差でしかありません。


5. もう2つの落とし穴

決済まわりには、二重課金以外にもよくある穴が2つあります。

落とし穴1: 金額をブラウザから受け取っている

こういう実装を、AI は書きがちです。

【危険】
ブラウザ: 「商品Aを、5000円で購入します」
サーバー: 「はい、5000円で決済を作成します」

画面の状態をそのままサーバーに渡す——素直で、正常に動きます。

しかしブラウザから送られてくる値は、利用者が自由に書き換えられます。開発者ツールを使えば、50001 に変えて送ることができます。

【正しい】
ブラウザ: 「商品Aを、1個購入します」
サーバー: 「商品Aの価格をデータベースで確認 → 5000円 → 決済を作成」

受け取るのは商品IDと数量だけ。価格はサーバー側で引き直す。 これが原則です。クライアントから来た金額は、絶対に信用しません。

あわせて、数量の上限(マイナスや異常な大きさを弾く)と在庫の確認も、サーバー側で行う必要があります。「数量 -1」で購入されると、返金が発生することがあります。

落とし穴2: 署名を検証していない

Webhook の受け口は、インターネットに公開されています。URLさえ分かれば、誰でもリクエストを送れます。

署名を検証していないと、こうなります。

  1. 攻撃者があなたのWebhookのURLを見つける(推測しやすい形が多い)
  2. 「支払いが成功しました」という形のデータを、自分で組み立てて送る
  3. あなたのアプリは、それを本物だと信じて商品を渡す

決済サービスは、通知に署名を付けています。これを検証すれば、本物かどうか判定できます。

実装上の注意: 署名の検証には、フレームワークが解析する前の「生のリクエストボディ」 が必要です。JSONとして解析してしまうと、文字列が微妙に変わって検証に失敗します。AI がこの点を見落とすと、「署名検証を入れたのに全部失敗する」という状態になり、そこで検証を外してしまう——という最悪の流れが起こり得ます。


6. 自分で確認する手順

コードが読めなくても、いくつかは確認できます。

確認1: 金額の改ざん

  1. 購入画面を開き、F12 で開発者ツールの「ネットワーク」タブを開く
  2. 購入ボタンを押す
  3. サーバーへ送られたリクエストの中身を見る

送信内容に「金額」「価格」「amount」「price」といった項目が含まれていたら要注意です。商品IDと数量だけなら正常です。

確認2: Webhookの署名検証

これはコードを見ないと分かりませんが、AI に聞けば答えられます(次章の指示文を使ってください)。

確認3: 二重処理

Stripe の管理画面には、Webhook イベントを手動で再送する機能があります。

  1. Stripe ダッシュボード → Developers → Webhooks
  2. 過去のイベントを選ぶ
  3. 「Resend(再送)」を実行する
  4. あなたのアプリのデータを確認する

残高が二重に増えた、注文が2つできた——なら、二重処理されています。何も変わらなければ、冪等性が効いています。

これが唯一の実地確認方法です。 公開前に必ず1回やってください。


7. AIに貼る修正指示

このアプリの決済まわりを、次の4点について確認して報告してください。

(1) Webhook の処理で、同じイベントが複数回届いたときに二重処理される
    可能性があるか
(2) 決済金額がクライアント(ブラウザ)から送られた値を使っていないか
(3) Webhook の署名検証が行われているか
(4) 数量や在庫の検証がサーバー側で行われているか

そのうえで、次のように修正してください。

■ 冪等性
イベントIDを一意キー(データベースの unique 制約付き)として保存し、
処理済みなら早期リターンする実装を追加してください。イベントIDの記録と
業務処理(残高の更新・注文の確定など)は同一トランザクションにまとめて
ください。処理済みで何もしなかった場合は 200 を返し、本当に処理に失敗
した場合のみエラーを返して決済サービスが再送できるようにしてください。

■ 金額
クライアントから金額を受け取っている場合は、商品IDと数量だけを受け取り、
価格はサーバー側でデータベースから引き直す実装に変更してください。数量
の上限とマイナス値の検証も追加してください。

■ 署名検証
未実装なら、決済サービス公式の署名検証を実装してください。その際、
フレームワークが本文を解析する前の生のリクエストボディを使うように
し、検証に失敗したら処理せず 400 を返してください。

私はコードが読めないので、各項目について「修正前は何ができてしまう
状態だったか」を日本語で1行ずつ説明してください。

8. 修正したあと、必ず再送テストを

AI が「修正しました」と言っても、それだけでは確認になりません。

Stripe の管理画面から、実際にイベントを再送してください。 データが二重にならなければ、冪等性が効いています。

このテストは1分で終わり、しかも本番で最も痛い事故を1つ完全に潰せます。費用対効果で言えば、この記事のどの作業よりも高いはずです。


まとめ

  • 決済サービスのWebhookは「最低1回」届く設計。複数回届くのは仕様であって障害ではない
  • 二重に届けないのは決済サービスの仕事ではなく、二重に処理しないのがあなたの仕事
  • 対策は冪等性。イベントIDを一意キーで保存し、処理済みなら何もしない。記録と業務処理は同一トランザクションで
  • 金額はブラウザから受け取らない。商品IDと数量だけ受け取り、価格はサーバー側で引き直す
  • Webhookの署名検証は必須。生のリクエストボディを使う点に注意
  • 確認方法は Stripe 管理画面からのイベント再送。公開前に必ず1回

決済は、AI で作ったアプリで最も実害が出やすい領域です。同時に、対策は定型的で、一度入れれば終わりです。

他に何を確認すべきかは AIで作ったアプリを公開する前に にまとめています。

よくある質問

なぜ決済の通知が複数回届くのですか?
決済サービスは「通知が確実に届く」ことを優先しているためです。ネットワークが不安定だったり、あなたのアプリの応答が遅れたり、一時的にエラーを返したりすると、決済サービスは『届かなかったかもしれない』と判断して再送します。これは at-least-once(最低1回)配信と呼ばれる設計で、Stripeの公式ドキュメントにも明記された仕様です。障害ではありません。
冪等性(べきとうせい)とは何ですか?
「何回やっても結果が同じになる性質」のことです。エレベーターのボタンを5回押しても1回分しか動かないのと同じ考え方です。決済処理では、通知に含まれるイベントIDをデータベースに保存し、すでに処理済みのIDが来たら何もせず正常応答を返すことで実現します。
手元でテストすれば二重課金は見つかりますか?
見つかりません。手元での動作確認では通知は1回しか来ないため、二重処理の分岐は一度も実行されません。この不具合は本番環境で、しかもネットワークが不安定なときや処理が混み合ったときに初めて出ます。テストで見つからないことが、この問題を厄介にしている最大の理由です。
金額をブラウザから送ると何が問題ですか?
リクエストを書き換えるだけで、好きな金額で購入できてしまいます。ブラウザから送られてくる値は、利用者が自由に変更できるからです。AIは画面の状態をそのままサーバーに渡す実装を書きがちで、通常の操作では正常に動くため気づきにくい欠陥です。対策は、受け取るのを商品IDと数量だけにして、価格はサーバー側でデータベースから引き直すことです。
Webhookの署名検証はなぜ必要ですか?
Webhookの受け口はインターネットに公開されているため、URLさえ分かれば誰でもリクエストを送れるからです。署名を検証していないと、第三者が「支払いが成功しました」という偽の通知を送るだけで、商品や残高をタダで受け取れます。決済サービスが提供する署名検証の関数を使い、検証に失敗したリクエストは処理せずエラーを返してください。

参考文献

友田

友田 陽大

経済産業大臣賞 受賞プロダクト開発者。TypeScript + Python + AWS で、SaaS・業界DX・実用レベルの生成AI(RAG)を、要件定義からインフラ・運用まで一人で完遂します。

すでに公開している/お金や個人情報が動いているなら

AIで作ったアプリを、エンジニアが読んで危険な箇所を教えます

コードが読めなくても大丈夫です。リポジトリを見て「今すぐ直すべきこと」を優先度つきで洗い出し、非エンジニア向けのレポートと、AIにそのまま貼れる修正指示をお渡しします。私自身 Claude Code で本番のB2B SaaSを作っている実務者なので、AIで作ったこと自体を否定はしません。

プロジェクト単位(請負)・技術顧問のどちらにも対応可能です。まずは30分の無料技術相談から。

最短ルート:カレンダーから直接予約

相談内容が固まっている方は、フォーム送信よりその場で日程を確定する方がスムーズです。下記から空き時間をお選びください。

  • 30分のオンライン無料相談
  • Google Meet / Zoom / Microsoft Teams
  • NDA 商談前締結可・無理な営業はいたしません
無料相談の空き枠を予約する

あわせて読みたい