Webhookとは
Webhookは「イベント発生時にHTTP POSTで通知を送る」仕組みです。決済完了・GitHub push・新規注文などのイベントが発生したとき、サービスが設定されたURLにJSONペイロードをPOSTします。受信側のアプリケーションはこの通知を受けてアクション(メール送信・DB更新等)を実行します。
Webhook受信エンドポイントの実装
import crypto from 'crypto';
import express from 'express';
const app = express();
// Webhookエンドポイントではrawボディが必要
app.post('/webhooks/stripe',
express.raw({ type: 'application/json' }),
async (req, res) => {
const signature = req.headers['stripe-signature'];
// 1. HMAC署名の検証
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
signature,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
console.error('署名検証失敗:', err.message);
return res.status(400).json({ error: '不正なWebhook' });
}
// 2. べき等性の確認(同じイベントIDを2回処理しない)
const alreadyProcessed = await db.processedEvents.findUnique({
where: { eventId: event.id }
});
if (alreadyProcessed) {
return res.status(200).json({ status: 'already_processed' });
}
// 3. イベントタイプごとの処理
try {
switch (event.type) {
case 'payment_intent.succeeded':
await handlePaymentSucceeded(event.data.object);
break;
case 'customer.subscription.deleted':
await handleSubscriptionCancelled(event.data.object);
break;
default:
console.log(`未処理のイベント: ${event.type}`);
}
// 4. 処理済みとして記録
await db.processedEvents.create({
data: { eventId: event.id, processedAt: new Date() }
});
res.status(200).json({ received: true });
} catch (err) {
console.error('Webhook処理エラー:', err);
// 500を返すとStripeがリトライする
res.status(500).json({ error: 'Processing failed' });
}
}
);
独自Webhookのセキュア実装
// Webhookを送信する側(署名の生成)
const generateHmacSignature = (payload, secret) => {
const hmac = crypto.createHmac('sha256', secret);
hmac.update(JSON.stringify(payload));
return `sha256=${hmac.digest('hex')}`;
};
const sendWebhook = async (url, event, payload) => {
const signature = generateHmacSignature(payload, process.env.WEBHOOK_SECRET);
await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': signature,
'X-Webhook-Event': event,
'X-Webhook-Delivery': crypto.randomUUID() // イベントID
},
body: JSON.stringify(payload)
});
};
// 受信側(署名の検証)
const verifyWebhookSignature = (body, signature, secret) => {
const expected = generateHmacSignature(JSON.parse(body), secret);
// タイミング攻撃を防ぐためcrypto.timingSafeEqualを使用
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
};
Webhookのリトライ戦略
Webhookの送信側は受信側が失敗(non-200レスポンス・タイムアウト)した場合に指数バックオフでリトライします。
- 1回目: 即時
- 2回目: 1分後
- 3回目: 5分後
- 4回目: 30分後
- 5回目: 2時間後
ローカル開発でのWebhookデバッグ
# ngrokを使ってローカルのWebhookエンドポイントを外部に公開
ngrok http 3000
# Stripe CLIを使ってStripeのWebhookをローカルに転送
stripe listen --forward-to localhost:3000/webhooks/stripe
まとめ
Webhookの実装で最も重要なのはHMAC署名検証とべき等性です。署名検証を省略すると偽のリクエストによる不正な処理が発生するリスクがあります。べき等性を実装してリトライによる二重処理を防ぎ、非同期でイベント処理をキューに入れることで高いスループットと信頼性を実現してください。