The Complete Guide to Testing WooCommerce Webhooks Locally

Last updated March 1, 202611 min read
TL;DR

To test WooCommerce webhooks locally:

  • Start your local webhook receiver (e.g. node server.js on port 3000)
  • Start a Hooklistener tunnel: hooklistener tunnel --port 3000
  • In WooCommerce Admin → Settings → Advanced → Webhooks, create a webhook pointing to your tunnel URL
  • Trigger an event (create a test order) — the webhook fires via wp-cron
  • If the webhook shows "Disabled", it failed 5 times — fix the issue and re-enable it

WooCommerce webhooks are notoriously unreliable during local development. They depend on WordPress's wp-cron system, auto-disable after failures, and there's no "send test" button. This guide walks through setup, delivery mechanics, and every debugging technique you'll need.

Why WooCommerce webhooks are difficult to test

WooCommerce's webhook system has quirks that don't exist in platforms like Stripe or GitHub:

  • wp-cron delivery — Webhooks aren't sent immediately. They're queued and delivered by WordPress's pseudo-cron system, which only runs when someone visits the site. On a dev store with no traffic, webhooks can be delayed indefinitely.
  • Auto-disable after 5 failures — WooCommerce automatically sets a webhook's status to "Disabled" after 5 consecutive delivery failures. If your tunnel was down or your handler crashed, your webhook silently stops working.
  • No test button — There's no way to send a test payload from the WooCommerce admin. You have to trigger real events (create orders, update products).
  • SSL verification — WooCommerce verifies SSL certificates by default. Self-signed certs or misconfigured tunnels cause silent failures.
  • Large payloads — WooCommerce sends the entire resource object (full order with all line items, metadata, etc.), which can be very large and cause timeouts.

How WooCommerce webhooks work

When an event occurs (e.g. a new order), WooCommerce:

  1. Schedules a wp-cron job to deliver the webhook
  2. On the next page load (or cron tick), sends an HTTP POST to your delivery URL
  3. Includes the full resource payload as JSON
  4. Signs the request with HMAC-SHA256 in the X-WC-Webhook-Signature header
  5. Expects a 2xx response within the server's timeout (usually 5-15 seconds)

Available topics follow the pattern resource.action:

TopicFires When
order.createdA new order is placed
order.updatedOrder status, items, or metadata changes
order.deletedAn order is trashed or permanently deleted
product.createdA new product is published
product.updatedProduct details change (price, stock, etc.)
customer.createdA new customer account is registered
coupon.createdA new coupon is created

WooCommerce also sends a ping event when you first create a webhook — this is useful to confirm connectivity before triggering real events.

1. Set up your local webhook handler

Create a handler that receives WooCommerce webhooks and verifies the HMAC signature:

server.js
const express = require("express");
const crypto = require("crypto");

const app = express();
const WC_WEBHOOK_SECRET = process.env.WC_WEBHOOK_SECRET;

app.post("/webhooks/woocommerce", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.headers["x-wc-webhook-signature"];
  const body = req.body; // Buffer

  // Verify HMAC-SHA256
  const expectedSignature = crypto
    .createHmac("sha256", WC_WEBHOOK_SECRET)
    .update(body)
    .digest("base64");

  if (signature !== expectedSignature) {
    console.error("WooCommerce HMAC verification failed");
    return res.status(401).send("Unauthorized");
  }

  const payload = JSON.parse(body.toString());
  const topic = req.headers["x-wc-webhook-topic"];
  const resource = req.headers["x-wc-webhook-resource"];

  console.log(`[${topic}] ${resource} #${payload.id}`);

  // Return 200 quickly — WooCommerce has a short timeout
  res.status(200).send("OK");
});

app.listen(3000, () => console.log("Listening on port 3000"));

2. Start a Hooklistener tunnel

hooklistener login
hooklistener tunnel --port 3000

Copy the tunnel URL (e.g. https://m3p8x2.hook.events). Your full delivery URL will be:

https://m3p8x2.hook.events/webhooks/woocommerce

Info:Hooklistener tunnels use valid TLS certificates, so WooCommerce's SSL verification passes without any extra configuration.

3. Create the webhook in WooCommerce

Option A: WooCommerce Admin UI

  1. Go to WooCommerce → Settings → Advanced → Webhooks
  2. Click Add webhook
  3. Set the Name (e.g. "Dev - Order Created")
  4. Set Status to Active
  5. Select the Topic (e.g. "Order created")
  6. Paste your Hooklistener tunnel URL as the Delivery URL
  7. Set a Secret — this is used for HMAC signing
  8. Choose API Version (latest is recommended)
  9. Click Save webhook

WooCommerce immediately sends a ping event to verify connectivity. Check your Hooklistener CLI — you should see a POST request arrive.

Option B: WooCommerce REST API

create-webhook.sh
curl -X POST "https://your-store.com/wp-json/wc/v3/webhooks" \
  -u "consumer_key:consumer_secret" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Dev - Order Created",
    "topic": "order.created",
    "delivery_url": "https://m3p8x2.hook.events/webhooks/woocommerce",
    "secret": "your-webhook-secret",
    "status": "active"
  }'

4. Trigger test events

Unlike Stripe or GitHub, WooCommerce has no "Send test webhook" button. You need to trigger real events:

  • Orders: Place a test order through your store's checkout, or create one in WooCommerce Admin → Orders → Add order
  • Products: Create or edit a product in WooCommerce Admin → Products
  • Customers: Register a new customer account on the storefront

Important:Webhooks are delivered via wp-cron, which only fires on page loads. If you create an order in the admin and nothing happens, visit the store frontend to trigger wp-cron. Or better yet, set up a real system cron job (see the debugging section below).

5. Debugging common WooCommerce webhook issues

Webhook stuck in "Disabled" status

WooCommerce disables webhooks after 5 consecutive delivery failures. This is the #1 issue developers hit. Your tunnel was down, your handler returned an error, or WooCommerce couldn't reach the URL.

Fix: Go to WooCommerce → Settings → Advanced → Webhooks, edit the webhook, change Status back to "Active", and click Save. Then make sure your tunnel is running and your handler returns 200.

wp-cron not firing (delayed webhooks)

WordPress's wp-cron only runs when someone visits the site. On a dev store with no traffic, webhooks can be delayed for hours.

Fix: Disable WordPress's pseudo-cron and use a real system cron job:

wp-config.php
// Add to wp-config.php
define('DISABLE_WP_CRON', true);

Then add a system cron job that hits wp-cron.php every minute:

# Add to crontab (crontab -e)
* * * * * curl -s https://your-store.com/wp-cron.php > /dev/null 2>&1

SSL verification failures

WooCommerce verifies SSL certificates when delivering webhooks. Self-signed certs or expired certs cause silent failures. Hooklistener tunnels use valid certificates, so this isn't an issue when using a tunnel.

Large payloads causing timeouts

WooCommerce sends the entire resource object. An order with 50 line items and extensive metadata can be a very large JSON payload. If your handler takes too long to process, WooCommerce considers it a failure. Return 200 immediately and process asynchronously.

6. Using WooCommerce's delivery logs

WooCommerce keeps a delivery log for each webhook. To view it:

  1. Go to WooCommerce → Settings → Advanced → Webhooks
  2. Click on the webhook name to edit it
  3. Scroll down to see the Webhook delivery logs

Each log entry shows:

  • The request URL, headers, and body that were sent
  • The response status code, headers, and body received
  • Duration and any error messages

You can also query delivery logs via the REST API:

curl "https://your-store.com/wp-json/wc/v3/webhooks/<webhook_id>/deliveries" \
  -u "consumer_key:consumer_secret"

Info:Combine WooCommerce's delivery logs (what was sent) with Hooklistener's request log (what was received) to pinpoint delivery issues. If WooCommerce shows "delivered" but Hooklistener didn't receive it, the issue is network-level.