By Devport Team | Last updated: 2026-10-04 | 9 min read

WooCommerce Webhooks: How to Create, Verify and Debug Them

Webhooks let WooCommerce push changes to your application the moment they happen, instead of your app polling the REST API. This guide covers creating webhooks, verifying their signatures, and debugging deliveries that fail or get disabled.

Table of Contents

  1. Creating a webhook
  2. Topics
  3. What a delivery looks like
  4. Verifying the signature
  5. Managing webhooks via the REST API
  6. Debugging and auto-disable

Creating a Webhook

  1. Go to WooCommerce → Settings → Advanced → Webhooks and click Add webhook.
  2. Set a Name, Status (Active, Paused or Disabled) and Topic.
  3. Enter the Delivery URL – an HTTPS endpoint in your app.
  4. Set a Secret; it is used to sign every payload. Store the same value in your app.
  5. Leave API version on the current WP REST API integration version (v3) so payloads match wc/v3 responses.

When you save an active webhook, WooCommerce sends a "ping" POST to the delivery URL with a form-encoded body webhook_id=<id>. Your endpoint should answer it with HTTP 200, otherwise WooCommerce reports an error.

Topics

ResourceTopics
Ordersorder.created, order.updated, order.deleted, order.restored
Productsproduct.created, product.updated, product.deleted, product.restored
Customerscustomer.created, customer.updated, customer.deleted
Couponscoupon.created, coupon.updated, coupon.deleted, coupon.restored
Customaction.<hook_name> – fires on any WordPress action, e.g. action.woocommerce_order_status_completed

order.updated fires on many internal saves, so expect several deliveries for one checkout. Make your handler idempotent (for example by storing the last processed date_modified per order).

What a Delivery Looks Like

Each delivery is a POST with a JSON body equal to the resource's REST API representation, and these headers:

The User-Agent is WooCommerce/<version> Hookshot (WordPress/<version>). Deliveries are queued through Action Scheduler by default, so they arrive shortly after the event rather than inside the customer's request.

Verifying the Signature

Always compute the HMAC over the raw request body – re-serialising parsed JSON changes the bytes and breaks the comparison.

Node.js (Express)

import crypto from 'node:crypto';
import express from 'express';

const app = express();

app.post('/hooks/woocommerce', express.raw({ type: '*/*' }), (req, res) => {
  const raw = req.body; // Buffer

  // The ping sent when the webhook is saved is form-encoded and unsigned
  if (raw.toString('utf8').startsWith('webhook_id=')) return res.sendStatus(200);

  const received = Buffer.from(req.get('X-WC-Webhook-Signature') || '');
  const expected = Buffer.from(
    crypto.createHmac('sha256', process.env.WC_WEBHOOK_SECRET).update(raw).digest('base64')
  );
  if (received.length !== expected.length || !crypto.timingSafeEqual(received, expected)) {
    return res.sendStatus(401);
  }

  const payload = JSON.parse(raw.toString('utf8'));
  const topic = req.get('X-WC-Webhook-Topic');
  // enqueue work here; respond fast
  console.log(topic, payload.id);
  res.sendStatus(200);
});

app.listen(3000);

PHP

<?php
$raw      = file_get_contents( 'php://input' );
$received = $_SERVER['HTTP_X_WC_WEBHOOK_SIGNATURE'] ?? '';
$expected = base64_encode( hash_hmac( 'sha256', $raw, getenv( 'WC_WEBHOOK_SECRET' ), true ) );

if ( str_starts_with( $raw, 'webhook_id=' ) ) { // ping on save
    http_response_code( 200 );
    exit;
}
if ( ! hash_equals( $expected, $received ) ) {
    http_response_code( 401 );
    exit;
}
$payload = json_decode( $raw, true );
// queue the work, then:
http_response_code( 200 );

Managing Webhooks via the REST API

Integrations often register their own webhooks on install. This needs a key with Write (or Read/Write) permission:

curl -X POST https://example.com/wp-json/wc/v3/webhooks -u ck_xxx:cs_xxx \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Order updated -> ERP",
    "topic": "order.updated",
    "delivery_url": "https://erp.example.com/hooks/woocommerce",
    "secret": "a-long-random-string",
    "status": "active"
  }'

A read-only key gets "The API key provided does not have write permissions." List existing webhooks with GET /wc/v3/webhooks and avoid creating duplicates on every deploy.

Debugging and Auto-Disable

See also: REST API authentication and wc/v3 endpoints reference.