Master WooCommerce webhooks from setup to integration. Learn webhook topics, payload structure, HMAC-SHA256 signature verification, building a Next.js receiver, and handling retries. This practical guide covers step-by-step setup, real-world code snippets, security hardening, and common production edge cases.
WooCommerce powers a significant portion of the world's e-commerce stores, and one of its most powerful — yet frequently underutilized — features is its webhook system. Webhooks allow your WooCommerce store to push real-time event notifications to external services the moment something meaningful happens: a new order is placed, a product is updated, a customer registers, or a subscription renews. Instead of constantly polling your store's REST API for changes, webhooks let your applications react instantly, saving server resources and dramatically reducing latency.
If you've ever built a fulfillment integration, synced inventory to a warehouse management system, triggered transactional emails through a third-party provider, or fed order data into an analytics pipeline, you've already experienced the problem webhooks solve. Without them, you're left writing polling loops — fragile, expensive, and often delayed by minutes. With WooCommerce webhooks, your external systems become first-class observers of your store's event stream, receiving structured JSON payloads the moment state changes occur.
This guide is a complete end-to-end walkthrough. You will learn how to configure webhooks inside the WooCommerce admin panel, understand every available webhook topic, dissect the payload structure, verify incoming requests using HMAC-SHA256 signature headers, build a production-ready receiver endpoint in Next.js, and implement retry-safe handling. By the end, you'll have a solid foundation for any webhook-driven integration your project demands.
Prerequisites
Before diving in, make sure you have the following in place:
- A WordPress installation with WooCommerce 3.x or later installed and active
- Administrator access to the WooCommerce dashboard
- Node.js 18+ and a Next.js 14+ project (App Router)
- Basic familiarity with TypeScript and REST API concepts
- A publicly accessible HTTPS URL for your receiver endpoint (use ngrok for local development)
- Optional: a tool like Postman or
curlfor manual testing
1. What Are Webhooks and Why Use Them?
A webhook is an HTTP callback — your server registers a URL, and when a predefined event fires, the source system (WooCommerce in this case) sends an HTTP POST request to that URL with a JSON body describing the event. This is the "don't call us, we'll call you" pattern of API design.
Compared to REST polling, webhooks are:
| Dimension | REST Polling | Webhooks |
|---|---|---|
| Trigger | Time-based (interval) | Event-based (real-time) |
| Latency | Up to poll interval (e.g., 60s) | Near-instant (milliseconds) |
| Server load | High — repeated requests | Low — only on events |
| Missed events | Possible if polling skips | Handled by retry logic |
| Implementation | Simple but wasteful | Requires receiver endpoint |
| Scalability | Degrades with volume | Scales with event volume |
WooCommerce webhooks are built directly into the core plugin (no extra extensions needed) and support a wide range of resource events across orders, products, customers, coupons, and more. Each outbound request is signed with an HMAC-SHA256 hash, giving your receiver the ability to cryptographically verify that the payload came from your store and was not tampered with in transit.
2. Setting Up Webhooks in WooCommerce Admin
Configuring a webhook takes less than two minutes in the WooCommerce dashboard:
- Log in to your WordPress admin panel.
- Navigate to WooCommerce → Settings → Advanced → Webhooks.
- Click Add webhook.
- Fill in the following fields:
- Name — A descriptive label (e.g.,
Order Created → Fulfillment API). - Status — Set to Active when ready; use Paused during development.
- Topic — The event to listen for (e.g.,
Order created). - Delivery URL — Your publicly reachable HTTPS endpoint.
- Secret — A strong random string used to sign the payload (store this securely).
- API Version — Use
WP REST API Integration v3for the most complete payloads.
- Name — A descriptive label (e.g.,
- Click Save webhook.
WooCommerce will immediately send a ping event to your delivery URL to confirm it is reachable. Your endpoint must return a 2xx HTTP status within 10 seconds, or WooCommerce will consider the delivery a failure.
Tip: During local development, run
ngrok http 3000to expose your Next.js dev server and use the generated HTTPS URL as your delivery URL.
3. Available Webhook Topics
WooCommerce organizes webhook events into topics that follow the pattern {resource}.{action}. Below is the full reference table:
| Topic | Trigger Description |
|---|---|
order.created | A new order is placed |
order.updated | An existing order is modified |
order.deleted | An order is permanently deleted |
order.restored | A trashed order is restored |
product.created | A new product is published |
product.updated | A product's data or stock changes |
product.deleted | A product is permanently deleted |
product.restored | A trashed product is restored |
customer.created | A new customer account is registered |
customer.updated | A customer's profile is modified |
customer.deleted | A customer account is deleted |
coupon.created | A new coupon code is created |
coupon.updated | A coupon is modified |
coupon.deleted | A coupon is permanently deleted |
action.woocommerce_checkout_order_processed | Custom action hook fires at checkout |
You can also register custom action topics by hooking into any WooCommerce or WordPress action using the action.{hook_name} format in the Topic field. This gives you granular control beyond the default resource events.
4. Understanding the Webhook Payload Structure
Every WooCommerce webhook delivers a standard HTTP POST request with the following headers:
Content-Type: application/json
X-WC-Webhook-Source: https://your-store.com/
X-WC-Webhook-Topic: order.created
X-WC-Webhook-Resource: order
X-WC-Webhook-Event: created
X-WC-Webhook-Signature: <base64-encoded HMAC-SHA256>
X-WC-Webhook-ID: 42
X-WC-Webhook-Delivery-ID: 1234567
The request body is a JSON object matching the WooCommerce REST API v3 response schema for the given resource. Here is an abbreviated order.created payload:
{
"id": 1024,
"status": "processing",
"currency": "USD",
"total": "149.99",
"billing": {
"first_name": "Jane",
"last_name": "Doe",
"email": "jane@example.com",
"address_1": "123 Main St",
"city": "Austin",
"state": "TX",
"postcode": "78701",
"country": "US"
},
"line_items": [
{
"id": 1,
"name": "Premium Widget",
"product_id": 88,
"quantity": 2,
"total": "149.99"
}
],
"date_created": "2026-09-18T09:00:00",
"meta_data": []
}
The payload schema is identical to what you would receive from a GET /wp-json/wc/v3/orders/{id} API call, so any existing WooCommerce REST client types you have will transfer directly.
5. Verifying the X-WC-Webhook-Signature Header with HMAC-SHA256
Security is non-negotiable. Before processing any webhook payload, you must verify the X-WC-Webhook-Signature header. WooCommerce computes this by applying HMAC-SHA256 over the raw request body using the webhook secret you configured, then base64-encoding the result.
Here is a reusable TypeScript utility for signature verification:
// lib/woocommerce/verify-webhook.ts
import { createHmac, timingSafeEqual } from "crypto";
/**
* Verifies an incoming WooCommerce webhook signature.
*
* @param rawBody - The raw, unparsed request body as a Buffer or string
* @param signature - The value of the X-WC-Webhook-Signature header
* @param secret - The webhook secret configured in WooCommerce admin
* @returns true if the signature is valid, false otherwise
*/
export function verifyWooCommerceWebhook(
rawBody: Buffer | string,
signature: string,
secret: string
): boolean {
if (!signature || !secret) return false;
const body = typeof rawBody === "string" ? Buffer.from(rawBody, "utf-8") : rawBody;
const expectedSignature = createHmac("sha256", secret)
.update(body)
.digest("base64");
const expectedBuffer = Buffer.from(expectedSignature, "utf-8");
const receivedBuffer = Buffer.from(signature, "utf-8");
// Use timing-safe comparison to prevent timing attacks
if (expectedBuffer.length !== receivedBuffer.length) return false;
return timingSafeEqual(expectedBuffer, receivedBuffer);
}
Using timingSafeEqual from Node's built-in crypto module is critical — a naive string comparison (===) leaks timing information that can be exploited to forge signatures.
6. Building a Next.js Webhook Receiver Endpoint
With verification in place, let's build the actual API route. In Next.js 14 with the App Router, create the file at app/api/webhooks/woocommerce/route.ts.
First, define the TypeScript types for common webhook payloads:
// types/woocommerce-webhooks.ts
export type WooWebhookTopic =
| "order.created"
| "order.updated"
| "order.deleted"
| "product.created"
| "product.updated"
| "customer.created"
| "customer.updated";
export interface WooOrderPayload {
id: number;
status: string;
currency: string;
total: string;
billing: {
first_name: string;
last_name: string;
email: string;
address_1: string;
city: string;
state: string;
postcode: string;
country: string;
};
line_items: Array<{
id: number;
name: string;
product_id: number;
quantity: number;
total: string;
}>;
date_created: string;
}
export interface WooProductPayload {
id: number;
name: string;
status: string;
stock_quantity: number | null;
regular_price: string;
sale_price: string;
sku: string;
}
Now, build the route handler:
// app/api/webhooks/woocommerce/route.ts
import { NextRequest, NextResponse } from "next/server";
import { verifyWooCommerceWebhook } from "@/lib/woocommerce/verify-webhook";
import type { WooWebhookTopic, WooOrderPayload, WooProductPayload } from "@/types/woocommerce-webhooks";
const WOO_WEBHOOK_SECRET = process.env.WOO_WEBHOOK_SECRET ?? "";
export async function POST(request: NextRequest): Promise<NextResponse> {
// 1. Read the raw body as a buffer BEFORE any parsing
const rawBody = Buffer.from(await request.arrayBuffer());
// 2. Extract signature and topic from headers
const signature = request.headers.get("x-wc-webhook-signature") ?? "";
const topic = request.headers.get("x-wc-webhook-topic") as WooWebhookTopic | null;
const source = request.headers.get("x-wc-webhook-source") ?? "unknown";
// 3. Verify the signature — reject immediately if invalid
if (!verifyWooCommerceWebhook(rawBody, signature, WOO_WEBHOOK_SECRET)) {
console.warn(`[WooWebhook] Invalid signature from ${source}`);
return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
}
// 4. Parse the verified body
let payload: unknown;
try {
payload = JSON.parse(rawBody.toString("utf-8"));
} catch {
return NextResponse.json({ error: "Malformed JSON" }, { status: 400 });
}
console.log(`[WooWebhook] Received topic: ${topic} from ${source}`);
// 5. Route to the appropriate handler
try {
switch (topic) {
case "order.created":
await handleOrderCreated(payload as WooOrderPayload);
break;
case "order.updated":
await handleOrderUpdated(payload as WooOrderPayload);
break;
case "product.updated":
await handleProductUpdated(payload as WooProductPayload);
break;
default:
console.log(`[WooWebhook] Unhandled topic: ${topic}`);
}
} catch (err) {
console.error(`[WooWebhook] Handler error for topic ${topic}:`, err);
// Return 500 so WooCommerce will retry the delivery
return NextResponse.json({ error: "Handler failed" }, { status: 500 });
}
// 6. Always return 200 once successfully processed
return NextResponse.json({ received: true }, { status: 200 });
}
async function handleOrderCreated(order: WooOrderPayload): Promise<void> {
console.log(`[WooWebhook] New order #${order.id} — ${order.billing.email} — $${order.total}`);
// TODO: Send to fulfillment API, update inventory, trigger email, etc.
}
async function handleOrderUpdated(order: WooOrderPayload): Promise<void> {
console.log(`[WooWebhook] Order #${order.id} updated — status: ${order.status}`);
// TODO: Sync status to CRM or analytics platform
}
async function handleProductUpdated(product: WooProductPayload): Promise<void> {
console.log(`[WooWebhook] Product #${product.id} updated — stock: ${product.stock_quantity}`);
// TODO: Sync inventory to warehouse management system
}
Add this to your .env.local:
WOO_WEBHOOK_SECRET=your_strong_random_secret_here
Important: Next.js App Router routes parse the body automatically for most content types. Reading
request.arrayBuffer()directly ensures you get the raw bytes that WooCommerce signed — never pass a pre-parsed body to your verification function.
7. Retry Logic and Failure Handling
WooCommerce has a built-in retry mechanism for failed webhook deliveries. Understanding it is essential for building resilient integrations:
- If your endpoint returns a non-2xx status, or if the connection times out (> 10 seconds), WooCommerce marks the delivery as failed.
- WooCommerce retries failed deliveries up to 5 times, with exponential back-off (approximately 1 minute, 2 minutes, 4 minutes, 8 minutes, 16 minutes).
- After 5 consecutive failures, WooCommerce automatically disables the webhook and logs the failure under WooCommerce → Status → Logs.
- The
X-WC-Webhook-Delivery-IDheader provides a unique ID per delivery attempt — log this for deduplication.
To make your handler idempotent (safe to call multiple times with the same payload), use the order or resource id from the payload as an idempotency key:
// lib/woocommerce/idempotency.ts
const processedDeliveries = new Set<string>();
/**
* Returns true if this delivery has already been processed.
* In production, replace the in-memory Set with a Redis key or database record.
*/
export function isDuplicateDelivery(deliveryId: string): boolean {
if (processedDeliveries.has(deliveryId)) return true;
processedDeliveries.add(deliveryId);
return false;
}
In your route handler, add this check immediately after signature verification:
const deliveryId = request.headers.get("x-wc-webhook-delivery-id") ?? "";
if (isDuplicateDelivery(deliveryId)) {
console.log(`[WooWebhook] Duplicate delivery ${deliveryId} — skipping`);
return NextResponse.json({ received: true }, { status: 200 });
}
In production, back the deduplication store with Redis (e.g., using ioredis) and set a TTL of 24–48 hours on each key to avoid unbounded memory growth.
8. Testing Your Webhook Integration
Local testing with ngrok:
# Terminal 1 — start your Next.js dev server
npm run dev
# Terminal 2 — expose it publicly
ngrok http 3000
Copy the https:// forwarding URL from ngrok and paste it as the Delivery URL in your WooCommerce webhook settings.
Simulating a webhook with curl:
# Compute signature (replace MY_SECRET with your webhook secret)
BODY='{"id":1,"status":"processing","total":"99.00","billing":{"email":"test@example.com"}}'
SECRET="MY_SECRET"
SIG=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -binary | base64)
curl -X POST https://your-ngrok-url/api/webhooks/woocommerce \
-H "Content-Type: application/json" \
-H "X-WC-Webhook-Topic: order.created" \
-H "X-WC-Webhook-Signature: $SIG" \
-H "X-WC-Webhook-Source: https://your-store.com/" \
-H "X-WC-Webhook-Delivery-ID: test-001" \
-d "$BODY"
Checking WooCommerce delivery logs:
Navigate to WooCommerce → Settings → Advanced → Webhooks, click your webhook, and scroll to the Recent Delivery section. Each entry shows the HTTP response code, response body, and delivery timestamp — invaluable for debugging.
Conclusion
WooCommerce webhooks are a powerful, built-in mechanism for building real-time integrations without the overhead of polling. Here are the key takeaways from this guide:
- Configure webhooks in WooCommerce admin — Set a strong secret, choose the right topic, and provide an HTTPS delivery URL.
- Know your topics — WooCommerce covers orders, products, customers, and coupons, and supports custom action hooks for advanced use cases.
- Always verify the signature — Use HMAC-SHA256 with
timingSafeEqualto authenticate every incoming request before processing. - Read the raw body first — Parse after verification, never before, to ensure the signature check operates on the exact bytes WooCommerce signed.
- Return 2xx immediately — Complete any heavy processing asynchronously (via a queue) and respond within 10 seconds to avoid timeouts.
- Implement idempotency — Use the Delivery ID for deduplication to handle WooCommerce's built-in retries gracefully.
- Monitor delivery logs — WooCommerce disables webhooks after 5 failures; set up alerting so you catch failures before they silently break your integration.
With these patterns in place, you have a production-ready foundation for any WooCommerce webhook-driven integration — whether it's order fulfillment, inventory synchronization, CRM updates, or real-time analytics pipelines.
Written by Dev Pattern LabsCore Engineering
Dev Pattern Labs is an engineering lab and digital consultancy specializing in custom Next.js web applications, WooCommerce REST API integrations, and workflow automation for growing businesses.
Recommended Companion Articles

WooCommerce REST API: Managing Orders and Inventory
Learn to manage WooCommerce orders and inventory programmatically via the REST API. Covers listing orders, updating status, adding notes, processing refunds, and batch stock management.

WooCommerce REST API: Complete Getting Started Guide
Learn how to authenticate, query, and manipulate your WooCommerce store data using the REST API — with real code examples in JavaScript and PHP.
