Skip to content

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/webhook
javascript
// 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 テストツール

コミュニティ

サポート

究極の AI 駆動 IDE 学習ガイド