ほかの Webhook の記事は、このブログのほかの記事も含め、どれも同じ前提から始まります。プロバイダーはイベントをすぐに送り、問題はその後で起きた、という前提です。WooCommerce は、リクエストがサーバーを出る前に、その前提を崩します。
イベントは、注文が入った時点では送信されません。キューに入れられ、そのキューは WordPress の cron が処理します。そして WordPress の cron は、cron ではありません。誰かがページを読み込んだときに動くのです。
ストアが繁盛していれば、遅れは最大で約 1 分で、気づくことはありません。訪問者が 1 時間に数人しかいないストアなら、「リアルタイム」のはずの注文通知は、次の訪問者がたまたま現れたときに届きます。
TL;DR
- 配信は、Action Scheduler の
woocommerce_deliver_webhook_asyncアクションとしてキューに入ります - Action Scheduler を動かすのは WP-Cron で、WP-Cron はスケジュールではなく、ページの読み込みをきっかけに実行されます
- そのため、トラフィックの少ないストアでは Webhook の配信が遅く、予測できなくなります。それでいて、どこも壊れてはいません
- 「WooCommerce は、配信が 5 回を超えて連続で失敗すると、Webhook を自動的に無効にします」
2xx、301、302以外のレスポンスは、すべて失敗です- セルフホストの WordPress での本当の対策は、WP-Cron を無効にし、システムの cron から動かすことです
まず、そもそも送信されたかを確認する
エンドポイントをデバッグする前に、まったく異なる 2 つの問題のどちらなのかをはっきりさせます。配信が失敗したのか、配信がまだ行われていないのかです。
WooCommerce → 設定 → 高度な設定 → Webhook(WooCommerce → Settings → Advanced → Webhooks)には、各 Webhook のステータスが表示されます。WooCommerce → ステータス → ログ(WooCommerce → Status → Logs)では、ソース webhooks-delivery の最近の配信が、レスポンスコード付きで表示されます。配信が一覧にあって失敗しているなら、以下のチェックリストの残りに進んでください。ログが空なのに注文は確かに存在するなら、配信はまだキューに残っています。
キューそのものを見るには、ツール → 予約済みアクション(Tools → Scheduled Actions)にある Action Scheduler の管理画面を開き、woocommerce_deliver_webhook_async で絞り込みます。タイムスタンプが過去の日時になっている実行待ちのアクションが、動かぬ証拠です。実行予定の時刻は過ぎているのに、何もそれを実行していないということです。
WP-Cron がこう動く理由
WordPress には、バックグラウンドで動くプロセスがありません。WordPress が cron と呼んでいるのは、リクエストが来るたびに確認される、期限の来たタスクの一覧です。そのため時計は、トラフィックが来たときにしか進みません。2005 年の共有ホスティングにとっては妥当な設計でしたが、時間が重要な処理には向きません。
Webhook にとっての具体的な影響は次のとおりです。
配信の遅れは、イベントではなくトラフィックに連動します。安定したトラフィックがあるストアなら、約 1 分以内に配信されます。夜間に注文が入り、朝まで誰も閲覧しない B2B のストアでは、キューに入った配信が何時間も放置されることがあります。
滞留は積み重なります。失敗したアクションは、テーブルに残り続けます。しばらく運用しているストアでは、Action Scheduler のテーブルが大きくなり、キューを処理するはずのリクエストそのものを遅くするほどになります。
ページの読み込みをきっかけにする処理は、キャッシュがあると当てになりません。WordPress の前にフルページキャッシュや CDN があると、多くのアクセスはそもそも PHP まで届きません。つまり、cron も進みません。
対策は WordPress の標準的な運用で、Webhook が重要なストアなら、どこでもやる価値があります。
// wp-config.php
define('DISABLE_WP_CRON', true);
# then drive it from a real scheduler, every minute
* * * * * cd /var/www/store && wp cron event run --due-now >/dev/null 2>&1
WP-CLI のパッケージが使えるなら、wp action-scheduler run で Action Scheduler のキューを直接処理することもできます。cron が本物のタイマーで動くようになれば、配信の遅れは、誰かが閲覧しているかどうかに左右されなくなります。
5 回を超えて失敗すると、Webhook は消える
ここが、一時的な問題を恒久的な問題に変えてしまう部分です。
「WooCommerce は、配信が 5 回を超えて連続で失敗すると、Webhook を自動的に無効にします」。失敗の定義は広く、「2xx、301、302 以外の HTTP ステータスコードを返すレスポンスは、すべて失敗です。たとえば、4xx や 5xx のレスポンスは失敗として数えられます」とされています。
これは小さな数字です。注文が途切れないストアなら、エンドポイントが 2 分間ダウンするデプロイだけで、その回数の配信を難なく使い切ってしまいます。そうなると、Webhook のステータスは無効に切り替わり、そのまま戻りません。メールも、管理画面の通知もなく、人の目に触れるものは何もありません。注文は作られ続けるのに何も送信されず、それは売上の少ない静かな日とまったく同じに見えます。
これは WooCommerce に限った話ではありません。n8n の Webhook が動かないでは、自動化ツール側の 7 つの原因を、実際に原因だった頻度の順に並べています。また、このキューのように処理が不均一なキューでは、順不同の配信が当たり前に起きます。それが HubSpot は eventId が一意とは限らないと明言しているのテーマです。リトライが実際に 2 回届いてしまったとき、注文した本人の目にそれがどう映るかは、ダブルブッキングに書いています。
これは Shopify がサブスクリプションを削除するのと同じ形の失敗ですが、さらに悪い点が 2 つあります。しきい値が Shopify の 8 回ではなく 5 回を超える連続失敗であること、そして警告メールがまったくないことです。
本番環境で WooCommerce を運用しているなら、Webhook のステータスを監視してください。REST API で Webhook を読み取り、status !== 'active' ならアラートを出す定期タスクは、コストがほとんどかからず、しかも無効化を知らせてくれる唯一の手段です。
署名検証
WooCommerce は X-WC-Webhook-Signature を送ります。これは、Webhook に設定したシークレットを使って生のリクエストボディから計算した HMAC-SHA256 を、base64 でエンコードしたものです。
hex ではなく base64 で、これは Shopify と同じ慣習であり、Stripe や GitHub とは逆です。各プロバイダーが選んだエンコーディングは、Webhook プロバイダー別リファレンスにあります。複数のプロバイダーで検証用のヘルパーを共有していると、このエンコーディングの違いが、ペイロードは完璧に見えるのに「署名が一向に一致しない」という問題のよくある原因になります。
import crypto from "node:crypto";
function verifyWooCommerce(raw: string, header: string | null, secret: string) {
if (!header) return false;
const expected = crypto.createHmac("sha256", secret).update(raw, "utf8").digest("base64");
const a = Buffer.from(expected);
const b = Buffer.from(header);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
どこでも同じですが、再シリアライズしたオブジェクトではなく、受信したバイト列をハッシュします。プロバイダーごとの比較は Stripe、GitHub、Shopify の Webhook 署名を検証するにあります。
WooCommerce 特有の落とし穴もあります。シークレットを指定せずに Webhook を作成すると、署名は自分で選んだものではなく、サイト自身の鍵情報から生成されます。すると、コードとは無関係の理由で検証が失敗します。シークレットは明示的に設定してください。
残りのチェックリスト
配信が存在し、それが失敗しているときは、以下を順に確認します。
ステータスは paused や disabled ではなく、active である必要があります。無効の状態なら、ほかのすべてが一度に説明できるので、最初にここを確認してください。
トピックが、想定しているイベントと一致している必要があります。order.created は注文の作成時に発火しますが、多くの決済ゲートウェイでは、それは注文がまだ pending の段階です。支払いを待ちたいなら、たいていはステータスを確認したうえでの order.updated が適しています。「Webhook が発火しない」という報告の多くは、実際には「想定と違うイベントで発火している」のです。
配信 URL には、手元のノート PC からだけでなく、サーバーからも到達できる必要があります。社内ネットワークの内側にあるストアや、アウトバウンド通信の制限が厳しいホストにあるストアは、ここで何も知らせずに失敗することがあります。
プラグインが、アクションを途中で打ち切っていることがあります。WooCommerce のフックはフィルターで変更でき、キャッシュ、セキュリティ、パフォーマンス系のプラグインが Action Scheduler に干渉する例が知られています。プラグインを無効にしてテストするのは地味ですが、決め手になることがよくあります。
Webhook の API バージョンも確認します。WooCommerce の Webhook のペイロードにはバージョンがあり、何年も前に作った v1 の Webhook は、v3 とは違う形のデータを送ります。WooCommerce を更新した後にハンドラーが失敗し始めたなら、その Webhook がどのバージョンに固定されているかを確認してください。
AnyHook の位置づけ
上の問題のうち 2 つは構造的なもので、どちらもハンドラーを改善しても直りません。
1 つ目は、連続失敗による無効化です。Webhook の URL を in.anyhook.net/you/woo に向けると、AnyHook がエッジから約 1 秒で 200 を返します。そのため、連続失敗のカウンターは、自分のエンドポイントの状態に左右されなくなります。エンドポイントへのリトライは WooCommerce ではなく AnyHook のスケジュールで行われ、Webhook は有効なままです。
2 つ目は、WooCommerce 自身の配信ログがストアの中にあり、ストアの保存期間に従い、管理画面の中にしかないことです。AnyHook はすべてのイベントをヘッダーとボディを含めて保存し、保存時に暗号化します。期間を指定した再送もできるので、デプロイ中の取りこぼしも、WordPress に触れずに復旧できます。
- 連続失敗が 1 回、5 回、20 回に達した時点で、失敗アラートが届きます。WooCommerce が送ってこない通知です
- 配信は指数バックオフでリトライされ、1 回きりの試行よりはるかに長く続きます
- 複数のプラットフォームから受信しているなら、base64 版の署名の代わりに、
AnyHook-Signatureヘッダー 1 つで検証できます
中継サービスでも直せないのは、その手前にあるキューです。WP-Cron が動いていなければ、イベントはどこにも届きません。まず cron を直してください。その後のことは、すべて中継サービスが引き受けます。
まとめ
主要なプラットフォームの中で、「Webhook が遅れている」がたいていバグでもエンドポイントの問題でもないのは、WooCommerce だけです。何よりも先に Action Scheduler のキューを確認し、ストアのトラフィックが少ないなら、cron を本物のスケジューラーに移してください。
そのうえで、シークレットを設定し、生のバイト列に対して base64 で検証し、Webhook 自身のステータスを監視します。数回の連続失敗という余裕は、大きくありません。しかも無効化は、何の知らせもなくやってきます。