Webhook ガイド
概要
Webhookは、特定のイベントが発生したときに指定されたURLに自動的にHTTPリクエストを送信するHTTPコールバック機能です。このガイドでは、Webhookの設定、使用、管理方法について説明します。
クイックスタート
基本概念
- Webhook URL: Webhookイベントを受信するエンドポイント
- イベントタイプ: Webhookをトリガーする特定のアクション
- ペイロード: Webhook URLに送信されるデータ
- 署名: Webhookの真正性を検証するセキュリティ機能
Webhookの作成
javascript
// Webhook設定の作成
const webhook = {
url: 'https://your-app.com/webhook',
events: ['user.created', 'user.updated', 'user.deleted'],
secret: 'your-webhook-secret',
active: true
};
// Webhookの登録
const response = await fetch('/api/webhooks', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer your-api-token'
},
body: JSON.stringify(webhook)
});サポートされているイベントタイプ
ユーザーイベント
user.created- ユーザー作成user.updated- ユーザー情報更新user.deleted- ユーザー削除user.login- ユーザーログインuser.logout- ユーザーログアウト
プロジェクトイベント
project.created- プロジェクト作成project.updated- プロジェクト更新project.deleted- プロジェクト削除project.shared- プロジェクト共有
ファイルイベント
file.uploaded- ファイルアップロードfile.downloaded- ファイルダウンロードfile.deleted- ファイル削除file.shared- ファイル共有
Webhookペイロード形式
標準ペイロード構造
json
{
"id": "evt_1234567890",
"type": "user.created",
"created": 1640995200,
"data": {
"object": {
"id": "user_123",
"email": "user@example.com",
"name": "John Doe",
"created_at": "2023-01-01T00:00:00Z"
}
},
"request": {
"id": "req_1234567890",
"idempotency_key": null
}
}イベント固有のペイロード
ユーザー作成イベント
json
{
"type": "user.created",
"data": {
"object": {
"id": "user_123",
"email": "user@example.com",
"name": "John Doe",
"role": "user",
"created_at": "2023-01-01T00:00:00Z",
"metadata": {
"source": "registration_form",
"campaign": "summer_2023"
}
}
}
}プロジェクト更新イベント
json
{
"type": "project.updated",
"data": {
"object": {
"id": "project_456",
"name": "Updated Project Name",
"description": "新しいプロジェクトの説明",
"status": "active",
"updated_at": "2023-01-01T12:00:00Z",
"previous_attributes": {
"name": "Old Project Name",
"description": "古いプロジェクトの説明"
}
}
}
}ファイルアップロードイベント
json
{
"type": "file.uploaded",
"data": {
"object": {
"id": "file_789",
"filename": "document.pdf",
"size": 1024000,
"content_type": "application/pdf",
"project_id": "project_456",
"uploaded_by": "user_123",
"uploaded_at": "2023-01-01T15:30:00Z",
"url": "https://files.yourapp.com/file_789"
}
}
}Webhook設定
設定オプション
javascript
const webhookConfig = {
// 必須設定
url: 'https://your-app.com/webhook',
events: ['user.created', 'project.updated'],
// オプション設定
secret: 'your-webhook-secret',
active: true,
// 配信設定
delivery: {
timeout: 30000, // 30秒
retry_attempts: 3,
retry_delay: 5000 // 5秒
},
// フィルター設定
filters: {
user: {
role: ['admin', 'user']
},
project: {
status: ['active']
}
},
// ヘッダー設定
headers: {
'X-Custom-Header': 'custom-value',
'Authorization': 'Bearer custom-token'
}
};環境別設定
javascript
// 開発環境
const devWebhook = {
url: 'https://dev.your-app.com/webhook',
events: ['*'], // すべてのイベント
secret: 'dev-webhook-secret',
delivery: {
timeout: 10000,
retry_attempts: 1
}
};
// 本番環境
const prodWebhook = {
url: 'https://your-app.com/webhook',
events: ['user.created', 'user.updated', 'project.created'],
secret: process.env.WEBHOOK_SECRET,
delivery: {
timeout: 30000,
retry_attempts: 5,
retry_delay: 10000
}
};Webhookの受信
Express.jsでの実装
javascript
const express = require('express');
const crypto = require('crypto');
const app = express();
// 生のボディを取得するためのミドルウェア
app.use('/webhook', express.raw({ type: 'application/json' }));
// Webhook署名の検証
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return signature === `sha256=${expectedSignature}`;
}
// Webhookエンドポイント
app.post('/webhook', (req, res) => {
const signature = req.headers['x-webhook-signature'];
const payload = req.body;
const secret = process.env.WEBHOOK_SECRET;
// 署名検証
if (!verifyWebhookSignature(payload, signature, secret)) {
console.error('無効なWebhook署名');
return res.status(401).send('Unauthorized');
}
try {
const event = JSON.parse(payload);
console.log('Webhookイベント受信:', event.type);
// イベントタイプに基づく処理
switch (event.type) {
case 'user.created':
handleUserCreated(event.data.object);
break;
case 'user.updated':
handleUserUpdated(event.data.object);
break;
case 'project.created':
handleProjectCreated(event.data.object);
break;
default:
console.log('未処理のイベントタイプ:', event.type);
}
res.status(200).send('OK');
} catch (error) {
console.error('Webhook処理エラー:', error);
res.status(400).send('Bad Request');
}
});
// イベントハンドラー
function handleUserCreated(user) {
console.log('新しいユーザーが作成されました:', user.email);
// ウェルカムメール送信など
}
function handleUserUpdated(user) {
console.log('ユーザー情報が更新されました:', user.email);
// プロフィール同期など
}
function handleProjectCreated(project) {
console.log('新しいプロジェクトが作成されました:', project.name);
// 通知送信など
}
app.listen(3000, () => {
console.log('Webhookサーバーがポート3000で起動しました');
});Next.jsでの実装
javascript
// pages/api/webhook.js
import crypto from 'crypto';
export default function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
const signature = req.headers['x-webhook-signature'];
const payload = JSON.stringify(req.body);
const secret = process.env.WEBHOOK_SECRET;
// 署名検証
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
if (signature !== `sha256=${expectedSignature}`) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = req.body;
try {
// イベント処理
await processWebhookEvent(event);
res.status(200).json({ received: true });
} catch (error) {
console.error('Webhook処理エラー:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
async function processWebhookEvent(event) {
switch (event.type) {
case 'user.created':
await sendWelcomeEmail(event.data.object);
break;
case 'project.updated':
await syncProjectData(event.data.object);
break;
default:
console.log('未処理のイベント:', event.type);
}
}
export const config = {
api: {
bodyParser: {
sizeLimit: '1mb',
},
},
};Python (Flask) での実装
python
from flask import Flask, request, jsonify
import hashlib
import hmac
import json
import os
app = Flask(__name__)
def verify_webhook_signature(payload, signature, secret):
"""Webhook署名を検証"""
expected_signature = hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
return signature == f"sha256={expected_signature}"
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-Webhook-Signature')
payload = request.get_data()
secret = os.environ.get('WEBHOOK_SECRET')
# 署名検証
if not verify_webhook_signature(payload, signature, secret):
return jsonify({'error': 'Invalid signature'}), 401
try:
event = json.loads(payload)
print(f"Webhookイベント受信: {event['type']}")
# イベント処理
process_webhook_event(event)
return jsonify({'status': 'success'}), 200
except Exception as e:
print(f"Webhook処理エラー: {e}")
return jsonify({'error': 'Internal server error'}), 500
def process_webhook_event(event):
"""Webhookイベントを処理"""
event_type = event['type']
data = event['data']['object']
if event_type == 'user.created':
handle_user_created(data)
elif event_type == 'user.updated':
handle_user_updated(data)
elif event_type == 'project.created':
handle_project_created(data)
else:
print(f"未処理のイベントタイプ: {event_type}")
def handle_user_created(user):
print(f"新しいユーザーが作成されました: {user['email']}")
# ウェルカムメール送信など
def handle_user_updated(user):
print(f"ユーザー情報が更新されました: {user['email']}")
# プロフィール同期など
def handle_project_created(project):
print(f"新しいプロジェクトが作成されました: {project['name']}")
# 通知送信など
if __name__ == '__main__':
app.run(debug=True, port=3000)セキュリティ
署名検証
javascript
// HMAC-SHA256署名の生成
function generateWebhookSignature(payload, secret) {
return crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
}
// 署名検証の実装
function verifyWebhookSignature(payload, receivedSignature, secret) {
const expectedSignature = generateWebhookSignature(payload, secret);
// タイミング攻撃を防ぐための安全な比較
return crypto.timingSafeEqual(
Buffer.from(receivedSignature),
Buffer.from(`sha256=${expectedSignature}`)
);
}IPアドレス制限
javascript
// 許可されたIPアドレスのリスト
const allowedIPs = [
'192.168.1.100',
'10.0.0.50',
'203.0.113.0/24' // CIDR記法もサポート
];
function isIPAllowed(clientIP) {
return allowedIPs.some(allowedIP => {
if (allowedIP.includes('/')) {
// CIDR記法の場合
return isIPInCIDR(clientIP, allowedIP);
} else {
// 単一IPの場合
return clientIP === allowedIP;
}
});
}
// Webhookエンドポイントでの使用
app.post('/webhook', (req, res) => {
const clientIP = req.ip || req.connection.remoteAddress;
if (!isIPAllowed(clientIP)) {
console.error('許可されていないIPからのアクセス:', clientIP);
return res.status(403).send('Forbidden');
}
// 通常のWebhook処理
// ...
});HTTPS強制
javascript
// HTTPS強制ミドルウェア
function requireHTTPS(req, res, next) {
if (!req.secure && req.get('x-forwarded-proto') !== 'https') {
return res.status(400).json({
error: 'HTTPS required for webhook endpoints'
});
}
next();
}
app.use('/webhook', requireHTTPS);エラーハンドリングとリトライ
リトライ機能
javascript
// Webhook配信のリトライロジック
class WebhookDelivery {
constructor(webhook, event) {
this.webhook = webhook;
this.event = event;
this.maxRetries = 5;
this.baseDelay = 1000; // 1秒
}
async deliver() {
let attempt = 0;
while (attempt < this.maxRetries) {
try {
const response = await this.sendWebhook();
if (response.status >= 200 && response.status < 300) {
console.log('Webhook配信成功');
return { success: true, attempt: attempt + 1 };
}
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
} catch (error) {
attempt++;
console.error(`Webhook配信失敗 (試行 ${attempt}):`, error.message);
if (attempt >= this.maxRetries) {
return { success: false, error: error.message, attempts: attempt };
}
// 指数バックオフでリトライ
const delay = this.baseDelay * Math.pow(2, attempt - 1);
await this.sleep(delay);
}
}
}
async sendWebhook() {
const payload = JSON.stringify(this.event);
const signature = this.generateSignature(payload);
return fetch(this.webhook.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': `sha256=${signature}`,
'User-Agent': 'YourApp-Webhook/1.0',
...this.webhook.headers
},
body: payload,
timeout: this.webhook.timeout || 30000
});
}
generateSignature(payload) {
return crypto
.createHmac('sha256', this.webhook.secret)
.update(payload)
.digest('hex');
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}デッドレターキュー
javascript
// 失敗したWebhookイベントの管理
class WebhookDeadLetterQueue {
constructor() {
this.failedEvents = [];
}
addFailedEvent(webhook, event, error) {
this.failedEvents.push({
id: event.id,
webhook_url: webhook.url,
event_type: event.type,
payload: event,
error: error,
failed_at: new Date().toISOString(),
retry_count: 0
});
}
async retryFailedEvents() {
const eventsToRetry = this.failedEvents.filter(
event => event.retry_count < 3
);
for (const failedEvent of eventsToRetry) {
try {
await this.retryEvent(failedEvent);
this.removeFailedEvent(failedEvent.id);
} catch (error) {
failedEvent.retry_count++;
failedEvent.last_retry_at = new Date().toISOString();
console.error('リトライ失敗:', error.message);
}
}
}
async retryEvent(failedEvent) {
// Webhook再送信ロジック
const delivery = new WebhookDelivery(
{ url: failedEvent.webhook_url },
failedEvent.payload
);
const result = await delivery.deliver();
if (!result.success) {
throw new Error(result.error);
}
}
removeFailedEvent(eventId) {
this.failedEvents = this.failedEvents.filter(
event => event.id !== eventId
);
}
getFailedEvents() {
return this.failedEvents;
}
}テストとデバッグ
Webhookテスト
javascript
// Webhookテスト用のモックサーバー
const express = require('express');
const app = express();
app.use(express.json());
// テスト用Webhookエンドポイント
app.post('/test-webhook', (req, res) => {
console.log('テストWebhook受信:');
console.log('Headers:', req.headers);
console.log('Body:', JSON.stringify(req.body, null, 2));
// レスポンス時間をシミュレート
setTimeout(() => {
res.status(200).json({ received: true });
}, 1000);
});
// エラーシミュレーション用エンドポイント
app.post('/test-webhook-error', (req, res) => {
console.log('エラーWebhook受信');
res.status(500).json({ error: 'Simulated error' });
});
// タイムアウトシミュレーション用エンドポイント
app.post('/test-webhook-timeout', (req, res) => {
console.log('タイムアウトWebhook受信');
// レスポンスを送信しない(タイムアウトをシミュレート)
});
app.listen(4000, () => {
console.log('Webhookテストサーバーがポート4000で起動しました');
});Webhookイベントの送信テスト
javascript
// Webhookイベント送信のテスト
async function testWebhookDelivery() {
const testEvent = {
id: 'evt_test_123',
type: 'user.created',
created: Math.floor(Date.now() / 1000),
data: {
object: {
id: 'user_test_123',
email: 'test@example.com',
name: 'Test User',
created_at: new Date().toISOString()
}
}
};
const webhook = {
url: 'http://localhost:4000/test-webhook',
secret: 'test-secret'
};
const delivery = new WebhookDelivery(webhook, testEvent);
const result = await delivery.deliver();
console.log('テスト結果:', result);
}
// テスト実行
testWebhookDelivery().catch(console.error);ngrokを使用したローカルテスト
bash
# ngrokのインストール
npm install -g ngrok
# ローカルサーバーを外部に公開
ngrok http 3000
# 表示されたURLをWebhook URLとして使用
# 例: https://abc123.ngrok.io/webhookjavascript
// ngrok使用時の設定例
const webhookConfig = {
url: 'https://abc123.ngrok.io/webhook',
events: ['user.created', 'user.updated'],
secret: 'test-webhook-secret'
};監視とログ
Webhook配信の監視
javascript
// Webhook配信メトリクスの収集
class WebhookMetrics {
constructor() {
this.metrics = {
total_deliveries: 0,
successful_deliveries: 0,
failed_deliveries: 0,
average_response_time: 0,
response_times: []
};
}
recordDelivery(success, responseTime) {
this.metrics.total_deliveries++;
if (success) {
this.metrics.successful_deliveries++;
} else {
this.metrics.failed_deliveries++;
}
this.metrics.response_times.push(responseTime);
this.updateAverageResponseTime();
}
updateAverageResponseTime() {
const times = this.metrics.response_times;
this.metrics.average_response_time =
times.reduce((sum, time) => sum + time, 0) / times.length;
}
getSuccessRate() {
if (this.metrics.total_deliveries === 0) return 0;
return (this.metrics.successful_deliveries / this.metrics.total_deliveries) * 100;
}
getMetrics() {
return {
...this.metrics,
success_rate: this.getSuccessRate()
};
}
}
// 使用例
const metrics = new WebhookMetrics();
// Webhook配信時にメトリクスを記録
async function deliverWebhookWithMetrics(webhook, event) {
const startTime = Date.now();
try {
const delivery = new WebhookDelivery(webhook, event);
const result = await delivery.deliver();
const responseTime = Date.now() - startTime;
metrics.recordDelivery(result.success, responseTime);
return result;
} catch (error) {
const responseTime = Date.now() - startTime;
metrics.recordDelivery(false, responseTime);
throw error;
}
}ログ設定
javascript
// 構造化ログの実装
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: 'webhook-service' },
transports: [
new winston.transports.File({ filename: 'webhook-error.log', level: 'error' }),
new winston.transports.File({ filename: 'webhook-combined.log' }),
new winston.transports.Console({
format: winston.format.simple()
})
]
});
// Webhook配信ログ
function logWebhookDelivery(webhook, event, result) {
const logData = {
webhook_id: webhook.id,
webhook_url: webhook.url,
event_id: event.id,
event_type: event.type,
success: result.success,
attempts: result.attempts,
response_time: result.responseTime
};
if (result.success) {
logger.info('Webhook配信成功', logData);
} else {
logger.error('Webhook配信失敗', {
...logData,
error: result.error
});
}
}ベストプラクティス
1. 冪等性の確保
javascript
// 重複イベント処理の防止
const processedEvents = new Set();
app.post('/webhook', (req, res) => {
const event = req.body;
const eventId = event.id;
// 重複チェック
if (processedEvents.has(eventId)) {
console.log('重複イベントをスキップ:', eventId);
return res.status(200).send('Already processed');
}
try {
// イベント処理
processWebhookEvent(event);
// 処理済みとしてマーク
processedEvents.add(eventId);
res.status(200).send('OK');
} catch (error) {
console.error('イベント処理エラー:', error);
res.status(500).send('Internal Server Error');
}
});2. 非同期処理
javascript
// キューを使用した非同期処理
const Queue = require('bull');
const webhookQueue = new Queue('webhook processing');
app.post('/webhook', (req, res) => {
const event = req.body;
// すぐにレスポンスを返す
res.status(200).send('Received');
// バックグラウンドで処理
webhookQueue.add('process-event', event, {
attempts: 3,
backoff: {
type: 'exponential',
delay: 2000
}
});
});
// ワーカープロセス
webhookQueue.process('process-event', async (job) => {
const event = job.data;
await processWebhookEvent(event);
});3. レート制限
javascript
// レート制限の実装
const rateLimit = require('express-rate-limit');
const webhookLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分
max: 100, // 最大100リクエスト
message: 'Too many webhook requests',
standardHeaders: true,
legacyHeaders: false
});
app.use('/webhook', webhookLimiter);4. 設定の検証
javascript
// Webhook設定の検証
function validateWebhookConfig(config) {
const errors = [];
// URL検証
if (!config.url || !isValidURL(config.url)) {
errors.push('有効なURLが必要です');
}
// HTTPS検証
if (!config.url.startsWith('https://')) {
errors.push('HTTPSのURLが必要です');
}
// イベント検証
if (!config.events || config.events.length === 0) {
errors.push('少なくとも1つのイベントタイプが必要です');
}
// シークレット検証
if (!config.secret || config.secret.length < 16) {
errors.push('シークレットは16文字以上である必要があります');
}
return {
valid: errors.length === 0,
errors
};
}
function isValidURL(string) {
try {
new URL(string);
return true;
} catch (_) {
return false;
}
}トラブルシューティング
よくある問題
1. 署名検証の失敗
javascript
// デバッグ用の署名検証
function debugSignatureVerification(payload, receivedSignature, secret) {
console.log('ペイロード:', payload);
console.log('受信した署名:', receivedSignature);
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
console.log('期待される署名:', `sha256=${expectedSignature}`);
const isValid = receivedSignature === `sha256=${expectedSignature}`;
console.log('署名検証結果:', isValid);
return isValid;
}2. タイムアウトエラー
javascript
// タイムアウト設定の調整
const webhookConfig = {
url: 'https://your-app.com/webhook',
timeout: 30000, // 30秒に延長
delivery: {
retry_attempts: 5,
retry_delay: 5000
}
};3. ネットワークエラー
javascript
// ネットワークエラーのハンドリング
async function handleNetworkError(error, webhook, event) {
console.error('ネットワークエラー:', error.message);
// DNS解決エラー
if (error.code === 'ENOTFOUND') {
console.error('DNS解決に失敗しました:', webhook.url);
return { retry: false, error: 'DNS resolution failed' };
}
// 接続タイムアウト
if (error.code === 'ETIMEDOUT') {
console.error('接続タイムアウト:', webhook.url);
return { retry: true, error: 'Connection timeout' };
}
// 接続拒否
if (error.code === 'ECONNREFUSED') {
console.error('接続が拒否されました:', webhook.url);
return { retry: true, error: 'Connection refused' };
}
return { retry: true, error: error.message };
}関連リソース
ドキュメント
ツールとライブラリ
- ngrok - ローカル開発用トンネリング
- Webhook.site - Webhookテスト用サービス
- Postman - API テストツール