WooCommerce webhooks: order topics, headers and signature checks
To test a WooCommerce webhook, set its Delivery URL to a free webhooks.sh bin (no signup, 48 hours, 100 requests a day) and each order or product payload shows up live with its X-WC-Webhook headers. WooCommerce waits up to 60 seconds per delivery and disables it after more than 5 failures in a row.
A WooCommerce webhook POSTs the same JSON the REST API returns for the order, product, customer or coupon, plus X-WC-Webhook headers, to the Delivery URL. Pointing that Delivery URL at a free bin and placing a test order shows you the exact request before you wire it to your handler. This page covers setting up a test webhook, the unsigned save ping, delivery headers, topics, X-WC-Webhook-Signature verification, and common reasons deliveries go missing.
Point a webhook at a free bin
- Create a free bin in the browser at view.webhooks.page or run
curl -X POST https://webhooks.sh/v1/binsto get aningest_urlandview_url. - In your WordPress admin, go to
WooCommerce > Settings > Advanced > Webhooks > Add webhook. - Set
StatustoActive,TopictoOrder created, the bin's ingest URL as theDelivery URL(for examplehttps://<bin>.webhooks.page/woocommerce), enter aSecret, and leaveAPI VersiononWP REST API Integration v3. - Save the webhook, and watch the initial save ping show up in the bin viewer.
- Place a test order in your store and watch the order JSON arrive.
You can also create the same webhook through the REST API at /wp-json/wc/v3/webhooks.
# 1. Create a free bin (or open view.webhooks.page)
curl -X POST https://webhooks.sh/v1/bins
# 2. Or create the webhook with the REST API
curl -X POST https://<store>/wp-json/wc/v3/webhooks \
-u <consumer_key>:<consumer_secret> \
-H "Content-Type: application/json" \
-d '{"name": "Order created to bin",
"topic": "order.created",
"delivery_url": "https://<bin>.webhooks.page/woocommerce",
"secret": "<secret>"}'
The save ping and the delivery headers
The first time you save a webhook with Status set to Active, WooCommerce sends a ping to the Delivery URL: a POST request with the body webhook_id=<id> and only a user agent, so it carries no X-WC-Webhook headers and no signature. If your URL doesn't answer with a 2xx status code, the admin shows Error: Delivery URL returned response code: <code>, so your handler needs to let this unsigned ping through and answer it with a 200.
Real deliveries are application/json POST requests sent with the user agent WooCommerce/<version> Hookshot (WordPress/<version>). Their body matches the REST API response for that resource in the API Version you picked (WP REST API Integration v3 by default, or v2 or v1; the deprecated Legacy API v3 option only appears when the legacy REST API is available).
| Header | Example | Use it for |
|---|---|---|
X-WC-Webhook-Source | https://example.com/ | Which store sent it |
X-WC-Webhook-Topic | order.created | Routing in your handler |
X-WC-Webhook-Resource | order | Resource type |
X-WC-Webhook-Event | created | Event type |
X-WC-Webhook-Signature | Base64 HMAC-SHA256 | Verifying the sender |
X-WC-Webhook-ID | 42 | The webhook's ID |
X-WC-Webhook-Delivery-ID | A hash | Matching log entries |
Topics: orders, products and custom actions
- The core topics are
created,updatedanddeletedfor coupons, customers, orders and products, pluscoupon.restored,order.restored,product.restoredandproduct.published. - The
Actiontopic fires on a WordPress action hook you name, such asaction.woocommerce_add_to_cart, and its payload holds the hook name and the hook's first argument. - Plugins can register custom topics using the
woocommerce_webhook_topic_hooksfilter.
Verify X-WC-Webhook-Signature
The X-WC-Webhook-Signature header is the base64-encoded HMAC-SHA256 hash of the raw request body, keyed with the webhook's Secret (if you leave Secret blank in the admin, WooCommerce saves a randomly generated secret, which then shows in the Secret field). Read the raw body and verify the signature before parsing the JSON, compare the digests in constant time, and remember to let the unsigned webhook_id=<id> save ping through on your endpoint.
Since X-WC-Webhook-Signature stays visible in the webhooks.sh bin viewer, you can copy a captured payload and signature from your bin into the HMAC signature verifier (set to Custom HMAC, SHA-256, and base64) to check that your secret produces the same hash.
<?php
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WC_WEBHOOK_SIGNATURE'] ?? '';
$expected = base64_encode(
hash_hmac('sha256', $payload, WC_WEBHOOK_SECRET, true)
);
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit;
}
Why deliveries go missing
- Deliveries run in the background by default as Action Scheduler jobs in the
woocommerce-webhooksgroup and only go out when the queue runs, and if a resource already has a delivery pending for that webhook, another change doesn't queue a second one. - WooCommerce counts
301and302responses as successful deliveries but doesn't follow redirects, so aDelivery URLthat redirects (likehttptohttps, or a missing trailing slash) drops the payload while WooCommerce counts it as delivered. - On a free bin, request bodies over 100KB return
413body_too_largeand requests past 100 in a day return429daily_cap_reached, and WooCommerce counts both responses as failed deliveries.
You can check each request and response in WooCommerce > Status > Logs under the source webhooks-delivery, or walk through our webhook not received guide if nothing hits your endpoint.
| Rule | Value |
|---|---|
| Response timeout | 60 seconds |
| Counts as success | 2xx, 301 or 302 |
| Redirects followed | None |
| Disabled after | More than 5 consecutive failures |
| Ping on first Active save | POST body webhook_id=<id>, unsigned |
| Default API Version | WP REST API Integration v3 |
Keep the URL or move to localhost
Free bins expire after 48 hours, and if a bin expires while your webhook is still Active, deliveries will fail and WooCommerce will disable the webhook after more than 5 failures in a row. Pause or delete the test webhook when you're done, or claim the bin with a free Google sign-in before it expires to keep the same URL and 30 days of rolling history. If you need a persistent endpoint on webhooks.host, Solo ($12 a month, see pricing) gives you 250,000 requests a month, 5MB bodies, and the wsh CLI to relay live deliveries straight to a handler on localhost.
FAQ
Where are webhooks in WooCommerce?
You manage them in the WordPress admin at WooCommerce > Settings > Advanced > Webhooks, or through the REST API at /wp-json/wc/v3/webhooks using a consumer key and consumer secret.
Why was my WooCommerce webhook disabled?
WooCommerce sets a webhook's Status to Disabled after more than 5 consecutive failed deliveries, meaning any response other than 2xx, 301 or 302. Fix the issue on your endpoint, then edit the webhook and set Status back to Active.
How do I verify a WooCommerce webhook signature?
Compute the base64-encoded HMAC-SHA256 hash of the raw request body using the webhook's Secret (for example base64_encode(hash_hmac('sha256', $payload, $secret, true)) in PHP) and compare it to the X-WC-Webhook-Signature header.
What is the webhook_id request WooCommerce sends?
That webhook_id=<id> POST is the unsigned ping WooCommerce sends the first time you save a webhook with Status set to Active. Have your endpoint answer it with an HTTP 200 so the admin saves the webhook without an error.
Why is my WooCommerce webhook delayed?
By default, WooCommerce queues webhook deliveries in the background as Action Scheduler jobs in the woocommerce-webhooks group, so they only go out when the queue runs. You can switch to immediate delivery with the woocommerce_webhook_deliver_async filter.
Which API version should a WooCommerce webhook use?
Use WP REST API Integration v3, which is the default for new webhooks. The webhook payload matches the REST API response for whichever version (v3, v2 or v1) you select.
Last updated
Try it in 10 seconds.
Create a free bin at view.webhooks.page — no signup, no install. Point your sender at the URL and watch requests land live. Bins expire after 48 hours.