2026年6月24日

2026年6月24日

WordPressのWebhookを実装して外部サービスと連携する方法

はじめに

WebhookはWordPressで特定のイベントが発生したとき(記事の公開・コメント投稿など)に外部サービスへリアルタイム通知を送る仕組みです。ポーリングと異なりサーバー負荷が低く、Slack通知・Zapier連携・Discordボットなど幅広い用途に活用できます。本記事では送信・受信両方のWebhook実装とセキュリティ対策を5ステップで解説します。

症状・原因

  • 記事を公開したことをチームメンバーにリアルタイムで通知したい
  • ZapierやMakeなどの外部自動化ツールとWordPressを連携させたい
  • 外部システムからWordPressのコンテンツを自動更新したいが安全な受信口がない

解決手順

ステップ1:記事公開時にSlack/DiscordへWebhookを送信する

<?php
// 記事が公開されたときにSlackに通知を送る
add_action( 'publish_post', 'my_notify_slack_on_publish', 10, 2 );

function my_notify_slack_on_publish( $post_id, $post ) {
    // 自動保存とリビジョンはスキップする
    if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
        return;
    }

    // Slack Incoming Webhook URLを設定する(環境変数や管理画面設定から取得する)
    $slack_webhook_url = get_option( 'my_slack_webhook_url', '' );
    if ( empty( $slack_webhook_url ) ) {
        return; // URLが未設定の場合はスキップする
    }

    $post_title = get_the_title( $post_id );
    $post_url   = get_permalink( $post_id );
    $author     = get_the_author_meta( 'display_name', $post->post_author );
    $thumbnail  = get_the_post_thumbnail_url( $post_id, 'medium' ) ?: '';

    // Slackのメッセージペイロードを組み立てる
    $payload = [
        'username'   => get_bloginfo( 'name' ),
        'icon_emoji' => ':wordpress:',
        'attachments' => [
            [
                'color'      => '#0073aa', // WordPress管理画面のブルー
                'title'      => $post_title,
                'title_link' => $post_url,
                'text'       => "新しい記事が公開されました :tada:\n著者: {$author}",
                'image_url'  => $thumbnail,
                'footer'     => get_bloginfo( 'name' ),
                'ts'         => time(),
            ],
        ],
    ];

    // wp_remote_postでSlackにPOSTリクエストを送る
    wp_remote_post( $slack_webhook_url, [
        'headers'     => [ 'Content-Type' => 'application/json' ],
        'body'        => wp_json_encode( $payload ),
        'timeout'     => 10,
        'blocking'    => false, // 非同期で送信してページ表示を遅らせない
        'data_format' => 'body',
    ] );
}

ステップ2:HMACシグネチャでWebhookのセキュリティを確保する

<?php
// 送信するWebhookにHMAC-SHA256署名を付けて受信側が検証できるようにする
function my_send_signed_webhook( $url, $payload_data ) {
    $payload_json = wp_json_encode( $payload_data );

    // HMAC-SHA256でシグネチャを生成する
    $secret    = get_option( 'my_webhook_secret', '' ); // 共有シークレット
    $timestamp = time();
    // タイムスタンプとペイロードを結合してハッシュ化する(リプレイ攻撃対策)
    $signature_string = "{$timestamp}.{$payload_json}";
    $signature        = hash_hmac( 'sha256', $signature_string, $secret );

    $response = wp_remote_post( $url, [
        'headers' => [
            'Content-Type'   => 'application/json',
            // X-Signatureヘッダーで署名を送信する
            'X-Signature'    => "t={$timestamp},v1={$signature}",
            'X-Webhook-ID'   => wp_generate_uuid4(), // 重複排除用ID
        ],
        'body'    => $payload_json,
        'timeout' => 15,
    ] );

    return $response;
}

// 受信側での署名検証関数
function my_verify_webhook_signature( $payload, $signature_header, $secret ) {
    // ヘッダーからタイムスタンプと署名を分解する
    parse_str( str_replace( ',', '&', $signature_header ), $parts );
    $timestamp = $parts['t'] ?? 0;
    $received_sig = $parts['v1'] ?? '';

    // タイムスタンプが5分以内かチェックする(リプレイ攻撃対策)
    if ( abs( time() - $timestamp ) > 300 ) {
        return false;
    }

    // 期待されるシグネチャを計算する
    $expected_sig = hash_hmac( 'sha256', "{$timestamp}.{$payload}", $secret );

    // タイミング攻撃を防ぐhash_equalsで比較する
    return hash_equals( $expected_sig, $received_sig );
}

ステップ3:Webhook受信エンドポイントをREST APIで実装する

<?php
// 外部サービスからのWebhookを受け取るREST APIエンドポイントを登録する
add_action( 'rest_api_init', 'my_register_webhook_endpoint' );

function my_register_webhook_endpoint() {
    register_rest_route( 'my-plugin/v1', '/webhook/receive', [
        'methods'             => WP_REST_Request::METHOD_POST,
        'callback'            => 'my_handle_incoming_webhook',
        // 認証はシグネチャで行うため公開エンドポイントにする
        'permission_callback' => '__return_true',
    ] );
}

function my_handle_incoming_webhook( WP_REST_Request $request ) {
    // リクエストボディを文字列として取得する
    $payload    = $request->get_body();
    $sig_header = $request->get_header( 'x-signature' );
    $secret     = get_option( 'my_webhook_secret', '' );

    // シグネチャを検証する
    if ( ! my_verify_webhook_signature( $payload, $sig_header, $secret ) ) {
        return new WP_REST_Response(
            [ 'error' => '署名が無効です' ],
            401
        );
    }

    $data = json_decode( $payload, true );
    if ( json_last_error() !== JSON_ERROR_NONE ) {
        return new WP_REST_Response( [ 'error' => 'JSONパースエラー' ], 400 );
    }

    // イベントの種類に応じて処理を分岐する
    $event = $data['event'] ?? '';
    switch ( $event ) {
        case 'order.completed':
            // 注文完了イベントを処理する
            my_process_order_completed( $data );
            break;
        case 'user.registered':
            // ユーザー登録イベントを処理する
            my_process_user_registered( $data );
            break;
        default:
            // 未知のイベントはログに記録する
            error_log( "Unknown webhook event: {$event}" );
    }

    // 受信成功を返す(外部サービスがリトライしないように素早く200を返す)
    return new WP_REST_Response( [ 'status' => 'received' ], 200 );
}

ステップ4:失敗したWebhookのリトライ処理を実装する

<?php
// Webhookの送信に失敗した場合は指数バックオフでリトライする
function my_send_webhook_with_retry( $url, $payload, $attempt = 1 ) {
    $max_attempts = 3; // 最大3回リトライする

    $response = wp_remote_post( $url, [
        'headers' => [ 'Content-Type' => 'application/json' ],
        'body'    => wp_json_encode( $payload ),
        'timeout' => 15,
    ] );

    $status_code = wp_remote_retrieve_response_code( $response );
    $is_success  = ! is_wp_error( $response ) && $status_code >= 200 && $status_code < 300;

    if ( ! $is_success && $attempt < $max_attempts ) {
        // 指数バックオフ(2^attempt * 60秒)で次のリトライをスケジュールする
        $delay = pow( 2, $attempt ) * 60; // 120秒, 240秒, ...

        wp_schedule_single_event(
            time() + $delay,
            'my_retry_webhook',
            [ $url, $payload, $attempt + 1 ] // 引数をarrayで渡す
        );

        // 失敗をログに記録する
        my_log_webhook_attempt( $url, $payload, $status_code, false );
        return false;
    }

    my_log_webhook_attempt( $url, $payload, $status_code, $is_success );
    return $is_success;
}

// スケジュールされたリトライイベントのハンドラ
add_action( 'my_retry_webhook', 'my_send_webhook_with_retry', 10, 3 );

ステップ5:WebhookのログをカスタムDBテーブルに記録する

<?php
// Webhookログ用のカスタムテーブルを作成する(プラグイン有効化時に実行)
register_activation_hook( __FILE__, 'my_create_webhook_log_table' );

function my_create_webhook_log_table() {
    global $wpdb;
    $table_name      = $wpdb->prefix . 'webhook_logs';
    $charset_collate = $wpdb->get_charset_collate();

    $sql = "CREATE TABLE {$table_name} (
        id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        webhook_url VARCHAR(500) NOT NULL,
        event_type VARCHAR(100) DEFAULT '',
        payload LONGTEXT DEFAULT '',
        response_code INT DEFAULT 0,
        success TINYINT(1) DEFAULT 0,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (id),
        KEY success (success),
        KEY created_at (created_at)
    ) {$charset_collate};";

    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    dbDelta( $sql ); // テーブルが存在しない場合のみ作成する
}

// Webhookの送受信ログをDBに記録するヘルパー関数
function my_log_webhook_attempt( $url, $payload, $status_code, $success ) {
    global $wpdb;
    $wpdb->insert(
        $wpdb->prefix . 'webhook_logs',
        [
            'webhook_url'  => substr( $url, 0, 500 ), // URLを500文字に制限する
            'event_type'   => $payload['event'] ?? '',
            'payload'      => wp_json_encode( $payload ),
            'response_code' => (int) $status_code,
            'success'      => $success ? 1 : 0,
            'created_at'   => current_time( 'mysql' ),
        ],
        [ '%s', '%s', '%s', '%d', '%d', '%s' ]
    );
}

注意事項

  • シークレットの管理: Webhookシークレットはwp-config.phpの定数かサーバー環境変数に保存し、データベースに平文で保存しないようにしてください。
  • 非同期送信: blocking => falseを使うと送信結果を確認できませんが、ページ表示速度には影響しません。重要な通知はblocking => trueでエラーを確認してください。
  • ログの保持期間: Webhookログは定期的に古いレコードを削除するcronジョブを設定してDBが肥大化しないようにしてください。

まとめ

Webhookの送受信をHMACシグネチャで保護し、失敗時のリトライ処理とDBログを組み合わせることで、堅牢な外部連携システムを構築できます。Slack・Discord・Zapier・MakeなどあらゆるWebhook対応サービスと安全に連携可能になります。関連記事:WordPressのカスタム投稿タイプにACFを活用する方法

お気軽にご相談ください

お見積りへ お問い合わせへ