WooCommerce webhooks: how they behave, and how to see them
WooCommerce's webhooks are capable and badly instrumented: the admin shows a delivery log per webhook, but not the payloads your handler is failing on, and several behaviours surprise everyone the first time. This page is the missing manual.
The lifecycle
A webhook is created in the admin (WooCommerce, then Settings,
Advanced, Webhooks) or through the REST API
(POST /wp-json/wc/v3/webhooks
with a topic, a delivery URL, and optionally a secret). The moment
it is created, WooCommerce sends a ping: a form-encoded
webhook_id body, just
to see the URL answer. From then on, each matching event queues a
delivery.
Topics cover four resources (orders, products, customers, coupons),
each with created, updated and deleted, plus custom topics mapping
any WordPress action hook as
action.hook_name. The
payload is the same JSON the REST API returns for that resource.
Delivery is wp-cron
Deliveries go out in background processing via wp-cron, not inline
with the request that caused them. Two consequences: a busy store
delivers within seconds, while a quiet one can sit on a delivery
until the next page view wakes wp-cron; and delivery timing is not
ordering, so an
order.updated can
arrive before the
order.created it
follows. Handlers should key on the payload's own data, never on
arrival order.
Failure, retries, and the five-strikes rule
A delivery counts as failed when the destination answers anything outside 2xx. After five consecutive failures, WooCommerce sets the webhook's status to disabled and stops delivering, without an email or a dashboard banner. This is the behaviour behind most "orders silently stopped syncing" incidents: the handler 500ed for an hour once, the webhook died, and nobody knew until the reconciliation report.
The blunt fix is a destination that cannot fail. A CommerceHook endpoint answers 2xx to every delivery and captures it, so the webhook stays alive no matter what state your real handler is in; you then work from the captured copy, replaying deliveries at the handler while you fix it.
The headers that matter
X-WC-Webhook-Topic: the event, e.g. order.updatedX-WC-Webhook-Source: which store sent itX-WC-Webhook-Signature: base64 HMAC-SHA256 of the body, keyed with the webhook's secretX-WC-Webhook-Delivery-ID: matches the delivery log entry in the admin
Debugging questions
- Why did my WooCommerce webhook stop firing?
- The most common cause is auto-disable: after five consecutive deliveries that did not get a 2xx response, WooCommerce sets the webhook to disabled and stops trying. Check WooCommerce > Settings > Advanced > Webhooks for its status. Pointing the webhook at an inspector endpoint that always answers 2xx prevents the counter ever tripping.
- Why are deliveries delayed?
- WooCommerce delivers webhooks in the background through wp-cron, which only runs when the site gets traffic. A quiet store can hold deliveries until the next visit. If timing matters, run a real cron job for wp-cron instead of relying on page loads.
- What is the webhook_id delivery with no event type?
- The ping. When a webhook is created, WooCommerce immediately POSTs a small form-encoded body (webhook_id=123) to the delivery URL to check it answers. It is expected, and it is the only non-JSON delivery a healthy webhook sends.
- How do I verify the signature?
- Each delivery carries X-WC-Webhook-Signature: a base64-encoded HMAC-SHA256 of the raw request body, keyed with the webhook secret set at creation. Compute the same HMAC over the exact bytes received and compare. A mismatch on an otherwise valid delivery usually means something between the store and your handler rewrote the body.
Seeing the deliveries
CommerceHook gives a WooCommerce store a stable HTTPS destination
and shows every delivery in an inspector: the topic badge, every
header including the signature, and the payload as a JSON tree or
raw. Registration happens
from the dashboard,
deliveries can be replayed at any handler, and
commercehook listen
streams them into the one on your laptop. Free for one endpoint, no
card.