MeWrite Docs

Stripe webhook signature verification failed

StripeのWebhook署名検証に失敗した場合のエラー

概要

StripeからのWebhookリクエストの署名検証に失敗した場合に発生するエラーです。リクエストの改ざん防止とセキュリティのために署名検証は必須です。

エラーメッセージ

Error: No signatures found matching the expected signature for payload
StripeSignatureVerificationError: Webhook signature verification failed
Error: Webhook Error: Unable to extract timestamp and signatures from header

解決策

1. 正しい署名検証の実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// Node.js / Express
import Stripe from 'stripe'
import express from 'express'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
const app = express()

// ⚠️ 重要: JSON parseの前にraw bodyが必要
app.post(
  '/webhook',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    const sig = req.headers['stripe-signature']!
    const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!

    let event: Stripe.Event

    try {
      event = stripe.webhooks.constructEvent(
        req.body,  // raw body
        sig,
        webhookSecret
      )
    } catch (err) {
      console.error('Webhook signature verification failed:', err)
      return res.status(400).send(`Webhook Error: ${err.message}`)
    }

    // イベントを処理
    switch (event.type) {
      case 'payment_intent.succeeded':
        const paymentIntent = event.data.object
        console.log('Payment succeeded:', paymentIntent.id)
        break
      case 'checkout.session.completed':
        const session = event.data.object
        await handleCheckoutComplete(session)
        break
    }

    res.json({ received: true })
  }
)

2. Next.js App Routerでの実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// app/api/webhook/route.ts
import { NextRequest, NextResponse } from 'next/server'
import Stripe from 'stripe'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)

export async function POST(req: NextRequest) {
  const body = await req.text()  // raw bodyを取得
  const sig = req.headers.get('stripe-signature')!

  let event: Stripe.Event

  try {
    event = stripe.webhooks.constructEvent(
      body,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET!
    )
  } catch (err) {
    return NextResponse.json(
      { error: 'Invalid signature' },
      { status: 400 }
    )
  }

  // イベント処理
  // ...

  return NextResponse.json({ received: true })
}

3. 開発環境でのテスト

1
2
3
4
5
6
7
8
# Stripe CLIでWebhookをフォワード
stripe listen --forward-to localhost:3000/api/webhook

# 出力されるWebhook Secretを使用
# whsec_xxxxx...

# テストイベントを送信
stripe trigger payment_intent.succeeded

4. よくある間違い

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// ❌ NG: body-parserでパース済みのbodyを使用
app.use(express.json())  // これより先にwebhookルートを定義

// ❌ NG: JSON.stringifyしたbodyを使用
stripe.webhooks.constructEvent(
  JSON.stringify(req.body),  // ダメ
  sig,
  secret
)

// ✅ OK: raw bodyを使用
app.post('/webhook', express.raw({ type: 'application/json' }), handler)

5. Webhook Secretの取得

1
2
3
4
5
# Stripe Dashboard > Developers > Webhooks > エンドポイント選択
# Signing secret を確認

# 環境変数に設定
STRIPE_WEBHOOK_SECRET=whsec_xxxxx

6. Python / Flaskでの実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import stripe
from flask import Flask, request, jsonify

stripe.api_key = os.environ['STRIPE_SECRET_KEY']
webhook_secret = os.environ['STRIPE_WEBHOOK_SECRET']

@app.route('/webhook', methods=['POST'])
def webhook():
    payload = request.get_data()  # raw body
    sig_header = request.headers.get('Stripe-Signature')

    try:
        event = stripe.Webhook.construct_event(
            payload, sig_header, webhook_secret
        )
    except ValueError as e:
        return jsonify({'error': 'Invalid payload'}), 400
    except stripe.error.SignatureVerificationError as e:
        return jsonify({'error': 'Invalid signature'}), 400

    # イベント処理
    if event['type'] == 'payment_intent.succeeded':
        payment_intent = event['data']['object']
        handle_payment_success(payment_intent)

    return jsonify({'received': True})

7. べき等性の実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 同じイベントの重複処理を防ぐ
const processedEvents = new Set<string>()

app.post('/webhook', async (req, res) => {
  const event = stripe.webhooks.constructEvent(/* ... */)

  if (processedEvents.has(event.id)) {
    return res.json({ received: true })
  }

  // DBでチェックする方が確実
  const existing = await db.stripeEvent.findUnique({
    where: { id: event.id }
  })
  if (existing) {
    return res.json({ received: true })
  }

  // 処理
  await db.stripeEvent.create({ data: { id: event.id } })
  processedEvents.add(event.id)

  // ...
})

関連エラー

関連エラー

最終更新: 2025-12-22