How to Test WooCommerce Webhooks Locally and in Production
WooCommerce ships with a built-in webhook system that fires on orders, products, customers, and coupons. If you are building a fulfillment integration, inventory sync, or CRM connector on top of a WooCommerce store, you need to understand exactly what WooCommerce sends and when it sends it.
Unlike Shopify, which hosts everything for you, WooCommerce runs on your client's WordPress server. That creates a different set of challenges when it comes to testing webhooks during development. This guide covers the full workflow: configuration, payload structure, local testing, and signature verification.
Where to configure WooCommerce webhooks
WooCommerce webhooks are configured in the WordPress admin under WooCommerce > Settings > Advanced > Webhooks. From there, click "Add webhook" to create a new one.
Each webhook has four key fields:
- Status: Active or Disabled. A webhook must be active to fire.
- Topic: The event that triggers the webhook. Topics are grouped by resource type (Order, Customer, Product, Coupon) and action (created, updated, deleted, restored).
- Delivery URL: The public HTTPS URL that will receive the POST request.
- Secret: A string used to generate the HMAC-SHA256 signature attached to every delivery.
- API version: WooCommerce REST API version (WP REST API v3 is current). This controls the shape of the payload.
WooCommerce also lets you create webhooks via the REST API itself if you are automating setup for a multi-tenant integration.
The webhook payload format
Stop reading raw JSON
Payloader shows you what your WooCommerce webhook actually did — in plain English. See the event, amount, status, and more at a glance.
Start free trial →
WooCommerce sends the full WooCommerce REST API object as the webhook body. This is a key point: the payload for an order.created event is identical to what you would get from a GET request to /wp-json/wc/v3/orders/{id}.
Here is the top-level structure of an order webhook payload:
{
"id": 1234,
"status": "processing",
"currency": "USD",
"total": "79.00",
"billing": {
"first_name": "Jane",
"last_name": "Smith",
"email": "[email protected]",
"address_1": "123 Main St",
"city": "Austin",
"state": "TX",
"postcode": "78701",
"country": "US"
},
"shipping": {
"first_name": "Jane",
"last_name": "Smith",
"address_1": "123 Main St",
"city": "Austin",
"state": "TX",
"postcode": "78701",
"country": "US"
},
"line_items": [
{
"id": 10,
"name": "Widget Pro",
"product_id": 55,
"quantity": 1,
"price": "79.00"
}
],
"shipping_lines": [...],
"meta_data": [...]
}
The full object is much larger than this. A real order payload routinely runs to several hundred lines of JSON once you include tax lines, fee lines, coupon lines, and meta data. Having a tool that renders this in a readable format saves a significant amount of time.
WooCommerce webhook headers
WooCommerce includes a set of custom headers on every delivery that identify the source and topic of the request:
X-WC-Webhook-Source: The URL of the WordPress site sending the webhook (e.g.,https://store.example.com/).X-WC-Webhook-Topic: The full event name in the formatresource.event, for exampleorder.createdorproduct.updated.X-WC-Webhook-Resource: The resource type alone (e.g.,order).X-WC-Webhook-Event: The action alone (e.g.,created).X-WC-Webhook-Signature: The HMAC-SHA256 signature of the raw request body, base64-encoded.X-WC-Webhook-ID: The internal WooCommerce ID of the webhook configuration.X-WC-Webhook-Delivery-ID: A unique ID for this specific delivery attempt.
Your application should use X-WC-Webhook-Topic to route incoming events to the correct handler, and X-WC-Webhook-Signature to verify authenticity before processing.
Capturing the payload with Payloader
WooCommerce's built-in delivery log is minimal. It records the HTTP status code and the response body your server returned, but it does not show you the full request payload or headers in a readable way. You have to trigger another event and hope your logging is set up correctly.
A better approach is to point the webhook at Payloader first. Payloader captures webhook requests at a public URL, shows you the complete headers and body in a human-readable format, and lets you replay and forward requests to your local server.
- Create a new endpoint in Payloader and copy the URL.
- Paste that URL into the "Delivery URL" field in your WooCommerce webhook settings.
- Save the webhook. WooCommerce will fire a test ping to confirm the URL is reachable.
- Trigger a real event: place a test order in WooCommerce, or use WooCommerce's built-in "Send test notification" button on the webhook detail page.
When the request arrives, Payloader displays the full JSON payload with syntax highlighting, all headers including the WooCommerce-specific ones, and the raw request body. You can immediately see whether the payload contains the fields your integration needs.
Testing locally
WooCommerce runs on your client's WordPress server, which is accessible on the public internet. Your local development machine is not. WooCommerce cannot POST a webhook to http://localhost:8000.
The standard solution is a tunneling tool like ngrok, but that requires running a separate process and updating the webhook URL in WooCommerce every time the tunnel restarts. Payloader's forwarding feature removes that friction.
Once Payloader is receiving your WooCommerce webhooks, configure the forwarding target to point at your local server (e.g., http://localhost:8000/webhooks/woocommerce/). Payloader will proxy each incoming request to your local machine in real time. If your handler throws an exception, click "Replay" to resend the exact same payload without having to create another test order.
Signature verification
WooCommerce signs every webhook delivery using HMAC-SHA256. The signature is computed over the raw request body using the secret you set in the webhook configuration. The result is base64-encoded and sent in the X-WC-Webhook-Signature header.
Always verify this signature before processing the payload. Here is how to do it in PHP (the natural choice for WordPress-adjacent code) and Python:
PHP:
$secret = 'your_webhook_secret';
$raw_body = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_WC_WEBHOOK_SIGNATURE'] ?? '';
$expected = base64_encode(hash_hmac('sha256', $raw_body, $secret, true));
if (!hash_equals($expected, $sig)) {
http_response_code(401);
exit('Invalid signature');
}
Python:
import hmac
import hashlib
import base64
def verify_woocommerce_signature(raw_body: bytes, secret: str, signature: str) -> bool:
expected = base64.b64encode(
hmac.new(secret.encode(), raw_body, hashlib.sha256).digest()
).decode()
return hmac.compare_digest(expected, signature)
Use a constant-time comparison function (hash_equals in PHP, hmac.compare_digest in Python) to prevent timing attacks. Read the raw request body before any framework parsing touches it, because frameworks may alter whitespace or key ordering.
Delivery failures and retries
WooCommerce considers a delivery successful if your server responds with an HTTP 2xx status code within a reasonable timeout. If the delivery fails (connection refused, timeout, or a non-2xx response), WooCommerce will retry up to five times with exponential backoff.
If all five retries fail, WooCommerce marks the webhook as "Disabled" automatically. The webhook will stop firing entirely until you manually re-enable it. To re-enable, go back to WooCommerce > Settings > Advanced > Webhooks, open the webhook, change the status back to "Active", and save.
This auto-disable behavior is important to know when you are testing. If your local development server is offline for a period while WooCommerce is trying to deliver, you may come back to find your webhook silently disabled. Check the webhook status in the admin panel if events stop arriving unexpectedly.
WooCommerce also logs each delivery attempt on the webhook detail page. The log shows the timestamp, HTTP status returned, and the response body your server sent. This is the first place to check when debugging a failed delivery.
Next steps
Once you have a handle on the payload structure and your signature verification is solid, the remaining work is application-specific: routing events to the right handlers, making your processing idempotent (WooCommerce may deliver the same event more than once), and responding quickly so you do not time out. Payloader's replay feature is useful throughout that process, letting you iterate on your handler logic without constantly generating new test orders in WooCommerce.