The Complete Guide to Testing WooCommerce Webhooks Locally
To test WooCommerce webhooks locally:
- Start your local webhook receiver (e.g.
node server.json 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:
- Schedules a wp-cron job to deliver the webhook
- On the next page load (or cron tick), sends an HTTP POST to your delivery URL
- Includes the full resource payload as JSON
- Signs the request with HMAC-SHA256 in the
X-WC-Webhook-Signatureheader - Expects a 2xx response within the server's timeout (usually 5-15 seconds)
Available topics follow the pattern resource.action:
| Topic | Fires When |
|---|---|
order.created | A new order is placed |
order.updated | Order status, items, or metadata changes |
order.deleted | An order is trashed or permanently deleted |
product.created | A new product is published |
product.updated | Product details change (price, stock, etc.) |
customer.created | A new customer account is registered |
coupon.created | A 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:
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 3000Copy the tunnel URL (e.g. https://m3p8x2.hook.events). Your full delivery URL will be:
https://m3p8x2.hook.events/webhooks/woocommerceInfo: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
- Go to WooCommerce → Settings → Advanced → Webhooks
- Click Add webhook
- Set the Name (e.g. "Dev - Order Created")
- Set Status to Active
- Select the Topic (e.g. "Order created")
- Paste your Hooklistener tunnel URL as the Delivery URL
- Set a Secret — this is used for HMAC signing
- Choose API Version (latest is recommended)
- 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
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:
// 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>&1SSL 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:
- Go to WooCommerce → Settings → Advanced → Webhooks
- Click on the webhook name to edit it
- 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.