Appearance
API-002-1 Stripe Webhook 受信
概要
Stripe からの Webhook(主に checkout.session.completed)を受信し、署名検証後に db_paid_article_purchase へ購入記録を冪等に INSERT する。
入力(リクエスト)
| 項目 | 必須 | 型・制約 | 説明 |
|---|---|---|---|
| HTTP メソッド | はい | POST | Stripe からの Webhook のみ |
Stripe-Signature ヘッダー | はい | 文字列 | t=...,v1=... 形式。Stripe\Webhook::constructEvent() で検証 |
| リクエストボディ | はい | 生 JSON 文字列 | Stripe Event オブジェクト。改変前のバイト列で署名検証する |
エンドポイント: POST /wp-json/commerce/v1/stripe-webhook
出力(レスポンス)
成功時 data(HTTP 200)
| field | 型 | 説明 |
|---|---|---|
received | bool | 常に true |
ignored | bool | 未対応イベント種別のとき true(購入記録は作成しない) |
失敗時 data(HTTP 400)
| field | 型 | 説明 |
|---|---|---|
received | bool | 常に false |
reason | string | invalid_signature / webhook_not_configured / invalid_payload |
レート制限時 data(HTTP 429)
| field | 型 | 説明 |
|---|---|---|
received | bool | 常に false |
reason | string | 常に rate_limited |
同一 IP から署名検証失敗が 15 分間に 20 回を超えた場合、Service / SDK 呼び出し前に 429 を返す(#2632)。
失敗・エラー条件
| 条件 | HTTP | reason |
|---|---|---|
| ボディが空 | 400 | invalid_payload |
STRIPE_WEBHOOK_SECRET 未設定 | 400 | webhook_not_configured |
| 署名検証失敗 | 400 | invalid_signature |
| 署名検証失敗の高頻度(IP 単位) | 429 | rate_limited |
権限・nonce
名前空間共通事項は API-一覧 を参照。
- WP ログイン・REST nonce は不要(
WordPressPublicPermissionCheckerが常に許可) - 実際の認証は Stripe 署名検証(
whsec_*+Stripe-Signatureヘッダー)が担う
レート制限・ログ記録(#2632)
- 一次防御: Stripe 署名検証(上記)
- 二次防御: 署名検証失敗を IP 単位で Transient カウント。15 分間に 20 回超過で HTTP 429(
reason: rate_limited) - ログ: 署名検証失敗のたびに
error_logで IP を記録(WP_DEBUG/WP_DEBUG_LOG有効時のみ。ペイロード内容は記録しない) - 正規配信: 署名成功リクエストはカウンタに加算されない(Stripe からの高頻度 Webhook バーストに影響なし)
- Stripe IP ホワイトリスト: 今回は見送り(署名検証 + 失敗時遮断で十分。IP リストの保守負担を回避)
処理フロー
StripeWebhookHandlerがcommerce/v1/stripe-webhookを登録WordPressRestApiAdapterがinclude_raw_body指定時に$request->get_body()を_raw_bodyとして Controller に渡すStripeWebhookServiceがStripe\Webhook::constructEvent()で署名検証checkout.session.completedのみ購入記録を作成metadata.user_id/metadata.post_id(#2630 Checkout 作成時に付与)external_provider = stripeexternal_transaction_id = session.id(uk_external_transaction_idで冪等)
- 購入記録が新規 INSERT された場合のみ、管理者(
admin_email)へ決済控えメールをwp_mailで送信する(#2703,PaidArticlePurchaseAdminNotifier)- 重複 INSERT(
null)ではメールを送らない - メール送信の成否は Webhook 応答に影響しない(失敗しても後続の HTTP 200 を維持し、
error_logに記録)
- 重複 INSERT(
- 処理成功時は 常に HTTP 200(Stripe の再送を防ぐ)。重複 INSERT 時も 200
決済未反映時の手動補正手順
決済は Stripe 側で完了しているが、サイト上で購入済みにならない場合の確認・補正手順。
1. 状況確認
- Stripe Dashboard(テスト/本番)→ Payments で該当決済が
Succeededか確認 - Developers → Webhooks でエンドポイント
.../wp-json/commerce/v1/stripe-webhookの配信ログを確認400: 署名シークレット不一致・STRIPE_WEBHOOK_SECRET未設定の可能性200だが未反映: DB または metadata 不整合の可能性
- WordPress DB の
wp_*_db_paid_article_purchaseにexternal_transaction_id = cs_*の行があるか確認
2. metadata の確認
Checkout Session の metadata に user_id と post_id が入っていること(#2630 実装どおり)。欠落している場合は Webhook では購入記録を作成せず 200 を返す(再送しても解消しない)。
3. 手動 INSERT(最終手段)
Stripe Dashboard で Session ID(cs_*)・金額・user_id・post_id を特定したうえで、管理者が DB に 1 件 INSERT する。
sql
INSERT INTO wp_db_paid_article_purchase
(user_id, post_id, price, external_provider, external_transaction_id, purchased_at)
VALUES
(<user_id>, <post_id>, <amount_jpy>, 'stripe', '<cs_session_id>', NOW());uk_external_transaction_id重複時は INSERT しない(既に反映済み)- 補正後、該当ユーザーで有料記事全文が閲覧できることを確認(
PaidArticleAccessService)
4. Webhook 再送
署名・シークレット修正後は Stripe Dashboard の Webhook ログから Resend で再送可能。冪等キーにより二重購入にはならない。
関連実装
| コンポーネント | パス |
|---|---|
| Handler | core_src/Handler/stripe_webhook_handler/StripeWebhookHandler.php |
| Controller | core_src/Controller/stripe_webhook_controller/StripeWebhookController.php |
| Service | core_src/Commerce/Service/stripe_webhook_service/StripeWebhookService.php |
| Gateway | core_src/Commerce/Infrastructure/Stripe/StripeWebhookEventGateway.php |
ローカル検証(Stripe CLI)
bash
stripe listen --forward-to https://<local-site>/wp-json/commerce/v1/stripe-webhook
# 表示される whsec_* を wp-config.php の STRIPE_WEBHOOK_SECRET に設定
stripe trigger checkout.session.completed