WooCommerce signs each webhook delivery with the Secret you set on the webhook (WooCommerce → Settings → Advanced → Webhooks). The header X-WC-Webhook-Signature is the base64-encoded HMAC-SHA256 of the raw request body, keyed with that secret. Other headers tell you what arrived: X-WC-Webhook-Topic, X-WC-Webhook-Resource, X-WC-Webhook-Event, X-WC-Webhook-ID, X-WC-Webhook-Delivery-ID and X-WC-Webhook-Source.
\/) and spacing. Hash the raw bytes.webhook_id=123, not an order or product payload. Answer it with 200 instead of failing hard, or the webhook may not activate cleanly.woocommerce_max_webhook_delivery_failures filter) WooCommerce sets the webhook status to Disabled. Fix the endpoint, then set it back to Active. Check WooCommerce → Status → Logs for the delivery log.DISABLE_WP_CRON), deliveries arrive late or not at all. Check Tools → Scheduled Actions for pending woocommerce_deliver_webhook_async actions.const crypto = require('crypto');
app.post('/webhooks/woocommerce', express.raw({ type: '*/*' }), (req, res) => {
const sig = req.get('X-WC-Webhook-Signature');
if (!sig) return res.sendStatus(200); // creation ping (webhook_id=...) - nothing to do
const expected = crypto.createHmac('sha256', process.env.WC_WEBHOOK_SECRET).update(req.body).digest('base64');
const a = Buffer.from(expected), b = Buffer.from(sig);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401);
const topic = req.get('X-WC-Webhook-Topic'); // e.g. order.created
const data = JSON.parse(req.body.toString('utf8'));
res.sendStatus(200);
});
import base64, hashlib, hmac, os
from flask import request, abort
@app.post('/webhooks/woocommerce')
def woo():
sig = request.headers.get('X-WC-Webhook-Signature')
if not sig: return '', 200 # creation ping
raw = request.get_data() # raw bytes
expected = base64.b64encode(hmac.new(os.environ['WC_WEBHOOK_SECRET'].encode(), raw, hashlib.sha256).digest()).decode()
if not hmac.compare_digest(expected, sig): abort(401)
data = request.get_json()
return '', 200
In FastAPI use raw = await request.body(). Only skip verification for a request with no signature header if you also ignore its contents; never process an order from an unsigned request.
The guide is Stripe-only. The kit is a self-hosted Cloudflare Worker relay that adds retries, replay and a dead-letter list in front of your endpoint. Its built-in schemes cover Stripe, Lemon Squeezy, Shopify, Paddle Billing, Square, Twilio (status callbacks) and GitHub. WooCommerce isn't one of them, so you would add a small verifier like the one above.
Fix Your Stripe Webhooks guide ($9) · Self-Hosted Webhook Relay Kit ($19)
Free download: Webhook debugging cheat sheet.
Fighting webhooks on more than one platform? The Fix Your Stripe Webhooks guide ($9) covers raw-body and signature problems in depth, and the Self-Hosted Webhook Relay Kit ($19) adds retries, replay and a dead-letter list in front of any endpoint. More fixes: all webhook error pages.