DEV Community

Cover image for Apidogを使ったCIでのStripe Webhookの受信・検証方法
Akira
Akira

Posted on • Originally published at apidog.com

Apidogを使ったCIでのStripe Webhookの受信・検証方法

顧客が支払いを完了すると、Stripe はバックエンドに payment_intent.succeeded イベントを送信します。通常はこのイベントを受けて注文を「支払い済み」に更新しますが、ハンドラが例外を投げても静かに失敗し、「支払い済みなのにアカウントでは未払い」という問い合わせが来るまで気付かないことがあります。CI では、イベントが到着し、正しく処理され、注文状態まで更新されたことを検証できるようにしましょう。

今すぐApidogを試す

問題は、Stripe ウェブフックが Stripe からバックエンドへのインバウンド HTTP リクエストである点です。一般的な API テストは「テストがリクエストを送り、レスポンスを検証する」形ですが、ウェブフックは逆方向です。CI で人手を介さず、非同期で届くイベントをどう検証するかが課題になります。

ここでは Apidog を使い、Stripe ウェブフックを CI で検証するための実装パターンを紹介します。準備として、ウェブフックをテストする方法 と Stripe のウェブフックドキュメントも確認してください。

制約: Apidog は Stripe ウェブフックを直接リッスンしない

Apidog のドキュメントには、Apidog はウェブフックのリッスンをネイティブサポートしていないと明記されています。

つまり、Stripe の送信先を Apidog に設定して、Apidog がリアルタイムにイベントを受信する構成は使えません。

CI 向けには、次のように責務を分けます。

  1. 自分のバックエンドで Stripe ウェブフックを受信する
  2. 受信イベントと処理結果をデータベースに保存する
  3. Apidog からデータベースをクエリする
  4. 保存されたイベントと業務状態をアサートする

この「キャプチャしてからクエリする」構成なら、非同期イベントを決定的なデータベース検証に変換できます。

全体構成: Capture → Persist → Query → Assert

実装する要素は4つです。

  1. Stripe からのイベントを受信するエンドポイント
  2. イベントを保存する stripe_event_logs テーブル
  3. Apidog の Post-Request Processor
  4. イベントログと注文状態を検証する SQL アサーション

バックエンド側で担当するのは、受信・署名検証・保存・業務処理です。Apidog 側では、保存済みデータを読み出して期待値と比較します。

Stripe
  ↓ payment_intent.succeeded
POST /webhooks/stripe
  ↓
署名検証
  ↓
stripe_event_logs に保存
  ↓
orders を支払い済みに更新
  ↓
Apidog Post-Request Processor
  ↓
DB をクエリしてイベント・注文状態を検証
Enter fullscreen mode Exit fullscreen mode

ステップ1: Stripe ウェブフック受信エンドポイントを実装する

Stripe が POST できるエンドポイントをバックエンドに作成します。

以下は Express を使った最小構成です。

import express from "express";
import Stripe from "stripe";

const app = express();
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;

app.post(
  "/webhooks/stripe",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    let event;

    try {
      event = stripe.webhooks.constructEvent(
        req.body,
        req.headers["stripe-signature"],
        endpointSecret
      );
    } catch (err) {
      return res.status(400).send(`Signature check failed: ${err.message}`);
    }

    // CI のテストで後から検証できるよう、イベントを保存する
    await db.query(
      `INSERT INTO stripe_event_logs (event_id, type, payload, handled_at)
       VALUES ($1, $2, $3, now())
       ON CONFLICT (event_id) DO NOTHING`,
      [event.id, event.type, JSON.stringify(event.data.object)]
    );

    if (event.type === "payment_intent.succeeded") {
      const intent = event.data.object;
      await markOrderPaid(intent.metadata.order_id);
    }

    res.json({ received: true });
  }
);
Enter fullscreen mode Exit fullscreen mode

実装時の重要ポイントは次の2つです。

1. Stripe 署名を必ず検証する

stripe.webhooks.constructEvent() で署名を検証してからイベントを処理してください。

署名検証では、JSON をパース済みのボディではなく、生のリクエストボディを使う必要があります。そのため Express では express.json() ではなく、対象ルートに express.raw() を設定しています。

署名検証の詳細は、ウェブフック署名検証も参照してください。

2. イベントを冪等に保存する

Stripe は同一イベントを複数回配信する場合があります。そのため、event_id を一意にし、ON CONFLICT DO NOTHING で重複保存を防ぎます。

最低限、次の情報を保存できるようにしておくと検証しやすくなります。

CREATE TABLE stripe_event_logs (
  event_id TEXT PRIMARY KEY,
  type TEXT NOT NULL,
  payload JSONB NOT NULL,
  handled_at TIMESTAMPTZ NOT NULL
);
Enter fullscreen mode Exit fullscreen mode

ステップ2: Apidog 環境にデータベース接続を設定する

Apidog で、テスト対象環境のデータベース接続を設定します。

たとえば、ステージング環境の API をテストするなら、Apidog からも同じステージング用データベースを参照する必要があります。

環境を一致させないと、次のような問題が起こります。

  • API はステージング環境にリクエストしている
  • Stripe はステージングのウェブフックエンドポイントにイベントを送る
  • しかし Apidog は別環境の DB をクエリしている
  • 結果として、イベントが正常に届いていてもテストは失敗する

テスト対象ごとに、API URL、Stripe の設定、DB 接続先を揃えてください。

ステップ3: Post-Request Processor でイベントログを検証する

Post-Request Processor は、リクエスト実行後にデータベースをクエリし、保存済みのウェブフックイベントを検証するために使います。

payment_intent.succeeded をテストする場合のフローは以下です。

  1. テストシナリオで支払いをトリガーする
  2. Stripe が /webhooks/stripe にイベントを配信する
  3. バックエンドがイベントを検証して stripe_event_logs に保存する
  4. Post-Request Processor がイベントログをクエリする
  5. イベント内容と注文状態をアサートする

イベントログを取得する SQL の例です。

SELECT event_id, type, payload, handled_at
FROM stripe_event_logs
WHERE type = 'payment_intent.succeeded'
ORDER BY handled_at DESC
LIMIT 1;
Enter fullscreen mode Exit fullscreen mode

この結果に対して、少なくとも次を確認してください。

  • typepayment_intent.succeeded
  • event_id が今回トリガーしたイベント ID と一致する
  • payload の金額が期待した金額と一致する
  • handled_at が設定されている

type だけを検証すると、過去のテスト実行で残ったイベントを拾う可能性があります。可能であれば、テストで作成した Payment Intent の ID、注文 ID、または Stripe のイベント ID で絞り込んでください。

SELECT event_id, type, payload, handled_at
FROM stripe_event_logs
WHERE event_id = :expected_event_id
  AND type = 'payment_intent.succeeded'
LIMIT 1;
Enter fullscreen mode Exit fullscreen mode

非同期配信に備えてポーリングを入れる

Stripe のウェブフック配信は同期処理ではありません。支払いをトリガーした直後に SQL を実行すると、イベントがまだ DB に保存されていない場合があります。

そのため、次のどちらかを入れてください。

  • 数秒の待機ステップ
  • イベントが見つかるまで数回再試行するポーリング

概念的には次のような動作です。

最大5回:
  stripe_event_logs を検索
  見つかったら検証して成功
  見つからなければ少し待って再試行

5回とも見つからなければ失敗
Enter fullscreen mode Exit fullscreen mode

短いリトライウィンドウを設けることで、テストと Stripe の配信タイミングが競合するフレークを減らせます。

イベントログだけでなく注文状態も検証する

イベントが保存されていることだけでは、「注文が支払い済みになった」ことまでは証明できません。

ハンドラが orders テーブルを更新するなら、2つ目のクエリで業務状態も検証してください。

SELECT id, payment_status, paid_at
FROM orders
WHERE id = :order_id;
Enter fullscreen mode Exit fullscreen mode

検証対象の例:

  • payment_statuspaid
  • paid_atNULL ではない
  • 対象注文 ID がテストで作成した注文 ID と一致する

これにより、次の一連の処理を CI で確認できます。

支払いトリガー
  → Stripe イベント配信
  → 署名検証
  → イベント保存
  → 注文状態更新
Enter fullscreen mode Exit fullscreen mode

ローカル開発では Stripe CLI または Ngrok を使う

キャプチャ・クエリパターンは、DB を使って決定的に検証できるため CI に向いています。

一方、ローカル開発では Stripe から localhost に直接イベントを送れません。ローカルでハンドラを実装・デバッグする場合は、イベント転送用のリレーを使います。

Stripe CLI の例:

stripe listen --forward-to localhost:3000/webhooks/stripe
Enter fullscreen mode Exit fullscreen mode

これにより、Stripe のイベントをローカルの /webhooks/stripe に転送できます。

Stripe CLI のほか、Ngrok でローカルポートを公開 URL にし、その URL を Stripe のエンドポイントとして登録する方法もあります。

使い分けは次のとおりです。

用途 推奨アプローチ
ローカルでハンドラを確認する Stripe CLI / Ngrok
CI でイベント処理を証明する イベント保存 + DB クエリ + Post-Request Processor

Apidog の Webhook 機能と混同しない

Apidog には Webhook という機能がありますが、これは Stripe から届くインバウンドイベントを受信するためのリスナーではありません。

Apidog のネイティブ Webhook 機能は、自分のシステムが外部 URL に送信するアウトバウンドウェブフックを定義・文書化するためのものです。

自分のアウトバウンドウェブフックを定義する場合は、次の手順を使います。

  1. 左サイドバーの + をクリックする
  2. New Other Protocol APIs を選択する
  3. Webhook を選択する
  4. Request MethodWebhook Name、必要に応じて Debug URL、リクエストボディ、ヘッダー、Other Info を設定する
  5. Save をクリックする

Debug URL に URL を入力して Send をクリックすれば、ウェブフック送信をシミュレートできます。

ただし、Debug URL はテスト専用であり、公開ドキュメントや OpenAPI エクスポートには表示されません。

イベントコールバックの設計・文書化については、API 設計におけるウェブフックも参照してください。

  • Apidog の Webhook 機能: 自分のアウトバウンドイベントを定義する
  • キャプチャ・クエリパターン: Stripe のインバウンドイベントを検証する

この2つは別の用途です。

CI 向けに堅牢化するポイント

基本のテストが動いたら、以下も追加してください。

実行ごとにイベントを特定する

過去のテストデータを誤検出しないように、次のいずれかを行います。

  • event_id で検索する
  • 注文 ID を payload 内で検索する
  • テスト開始前に対象データを削除する
  • テスト実行 ID を保存して検索条件に使う

イベントログを毎回全削除する場合は、並列実行するテストとの競合に注意してください。可能なら、全体を TRUNCATE するよりも、注文 ID やイベント ID でスコープする方が安全です。

失敗パスもテストする

ハッピーパスだけでは不十分です。たとえば次を確認します。

  • 不正な署名を拒否する
  • 想定外のイベントタイプを適切に扱う
  • 同一イベントの再送で注文が二重更新されない
  • 失敗時に handled_at を書き込まない、または失敗状態を記録する

支払いウェブフックの冪等性やリトライ設計については、支払いウェブフックのベストプラクティスも確認してください。

マージ時だけでなく定期実行する

シナリオを Apidog に保存したら、マージゲートだけでなく定期実行も検討してください。

デプロイや設定変更によってウェブフック処理が壊れた場合も検知しやすくなります。Apidog で API テストをスケジュールする方法を参照してください。

Apidog CLI で CI に組み込む

保存したシナリオは Apidog CLI からヘッドレス実行できます。

まず CLI をインストールし、アクセストークンで認証します。

npm install -g apidog-cli
apidog login --with-token <YOUR_ACCESS_TOKEN>
Enter fullscreen mode Exit fullscreen mode

次に、ウェブフック検証シナリオを CI で実行します。

apidog run --access-token $APIDOG_ACCESS_TOKEN -t <SCENARIO_ID> -e <ENV_ID> -r cli
Enter fullscreen mode Exit fullscreen mode

主なオプションは以下です。

  • -t: テストシナリオ ID
  • -e: 環境 ID
  • -r: レポーター

HTML レポートも出力したい場合は、次のように指定します。

apidog run \
  --access-token $APIDOG_ACCESS_TOKEN \
  -t <SCENARIO_ID> \
  -e <ENV_ID> \
  -r html,cli
Enter fullscreen mode Exit fullscreen mode

シナリオに Post-Request Processor と DB アサーションが含まれていれば、この1コマンドで以下を実行できます。

  1. 支払いフローをトリガーする
  2. Stripe ウェブフックの到着を待つ
  3. stripe_event_logs をクエリする
  4. イベント内容を検証する
  5. orders の支払い状態を検証する
  6. 失敗時に非ゼロ終了コードを返す

CLI の設定は Apidog CLI インストールガイド、GitHub Actions への組み込みは CI/CD パイプラインウォークスルーを参照してください。

よくある質問

Apidog は Stripe ウェブフックを直接受信できますか?

いいえ。Apidog はウェブフックをネイティブにリッスンしません。

Stripe のイベントは自分のバックエンドで受信し、データベースへ保存してください。その後、Apidog の Post-Request Processor から DB をクエリして検証します。ローカル開発では Stripe CLI や Ngrok を使ってイベントを転送します。

アサーションはどこで実行しますか?

テストシナリオの Post-Request Processor 内で実行します。

Apidog 環境に設定したデータベース接続を使い、stripe_event_logs をクエリして保存済みイベントを取得し、期待値と比較します。

データベース検証に有料プランは必要ですか?

このワークフローに関する Apidog のドキュメントには、プラン別の制限は記載されていません。実際の利用可否は、現在の料金ページとプラン詳細を確認してください。

Apidog をダウンロードしてテストプロジェクトを作成し、環境の DB 接続と Post-Request Processor を確認できます。

トリガーから配信までの遅延はどう扱いますか?

クエリ前に短い待機を入れるか、ポーリングによるリトライを実装してください。

数秒間に数回リトライする構成であれば、Stripe の非同期配信と競合しにくくなります。一般的な設計はウェブフックをテストする方法も参照してください。

Apidog のネイティブ Webhook 機能は使えませんか?

Stripe から届くイベントの受信には使いません。

ネイティブ Webhook 機能は、自分のシステムから外部 URL に送るアウトバウンドウェブフックを定義・文書化するためのものです。Stripe のインバウンドイベント検証には、キャプチャ・クエリパターンを使ってください。

まとめ

Stripe のウェブフックを Apidog が直接ライブ受信する構成は使えません。

CI では、次のパターンを採用してください。

  1. Stripe ウェブフックを自分のエンドポイントで受信する
  2. Stripe 署名を検証する
  3. イベントを stripe_event_logs に冪等に保存する
  4. 注文などの業務状態を更新する
  5. Apidog の Post-Request Processor で DB をクエリする
  6. イベント内容と注文状態の両方を検証する
  7. apidog run で CI のマージゲートに組み込む

この構成により、「イベントが届いた」だけでなく、「実際の支払いイベントによって注文が支払い済みに更新された」ことまで自動テストで証明できます。

Top comments (0)