Webhooks
A webhook is the REST API in reverse: instead of an external program asking the shop, the shop itself sends an HTTP request to a given address when something happens. An order is created — the warehouse knows a second later, without constant polling.
Creating in the admin panel
Open WooCommerce → Settings → Advanced → Webhooks and press Add webhook:
| Field | What to enter |
|---|---|
| Name | Anything, for yourself: Orders to the warehouse |
| Status | Active |
| Topic | The event: Order created |
| Delivery URL | The receiver's address: https://erp.example.com/woo-webhook.php |
| Secret | A string for signing requests |
After saving, every new order is sent to that address with a POST request.
Topics
| Topic | When it is sent |
|---|---|
order.created | An order is created |
order.updated | An order is changed, including its status |
product.updated | A product is changed |
customer.created | A customer is registered |
coupon.created | A coupon is created |
Every entity has the created, updated and deleted events.
What arrives
The request body is JSON of the same structure as a REST API response: for the order.created topic it is the whole order. Details of the event are in the headers:
| Header | What it holds |
|---|---|
X-WC-Webhook-Topic | The topic: order.created |
X-WC-Webhook-Signature | The signature of the request body |
X-WC-Webhook-Delivery-ID | The delivery number |
X-WC-Webhook-Source | The shop address |
The receiver
A webhook is received by another program. Here is a minimal PHP receiver — the file lives on the accounting system's server, not in the plugin:
// erp.example.com/woo-webhook.php
$secret = 'your-webhook-secret';
$body = file_get_contents('php://input');
$expected = base64_encode(hash_hmac('sha256', $body, $secret, true));
$actual = $_SERVER['HTTP_X_WC_WEBHOOK_SIGNATURE'] ?? '';
if (!hash_equals($expected, $actual)) {
http_response_code(401);
exit;
}
$order = json_decode($body, true);
file_put_contents(__DIR__ . '/orders.log', $order['id'] . ' ' . $order['total'] . PHP_EOL, FILE_APPEND);
http_response_code(200);The signature is an HMAC-SHA256 of the request body with the secret, encoded in Base64. The receiver computes it itself and compares it with the header: a match proves that your shop sent the request. Place an order — a line with the number and the total appears in orders.log.
Creating in code
A webhook is a WC_Webhook object. The file scripts/create-webhook.php:
// wp-content/plugins/acme-shop/scripts/create-webhook.php
$webhook = new WC_Webhook();
$webhook->set_name('Orders to the warehouse');
$webhook->set_user_id(1);
$webhook->set_topic('order.created');
$webhook->set_delivery_url('https://erp.example.com/woo-webhook.php');
$webhook->set_secret('your-webhook-secret');
$webhook->set_status('active');
$webhook->save();Run the file with wp eval-file — the webhook appears in the list in the admin panel. The user from set_user_id() defines the permissions with which the data to send is collected.
How delivery works
The request is sent not at the moment of checkout but slightly later, by a background job — the customer does not wait for someone else's server. The receiver must answer with a code in the 200–299 range. After five failed deliveries in a row WooCommerce disables the webhook, and it is enabled again in the admin panel.
The delivery log is in WooCommerce → Status → Logs, the webhooks-delivery source.
Webhook or REST API
| Task | What to choose |
|---|---|
| Learn about an event at once | A webhook |
| Get data on request | The REST API |
| Change data in the shop | The REST API |
| Reconcile everything once a day | The REST API with modified_after |
They are usually combined: a webhook reports the event, and once a day the integration reconciles through the REST API in case deliveries were lost.
Common mistake
The receiver does not verify the signature and processes whatever arrives at the address. A webhook address becomes known sooner or later, and anyone can send a forged paid order. The hash_equals() check with the secret is mandatory — and the signature must be computed from the raw request body, not from JSON that was parsed and assembled again.