diff --git a/README.md b/README.md
index 7419282..090fcd5 100644
--- a/README.md
+++ b/README.md
@@ -45,6 +45,7 @@ Skills for receiving and verifying webhooks from specific providers. Each includ
| Shopify | [`shopify-webhooks`](skills/shopify-webhooks/) | Verify Shopify HMAC signatures, handle order and product webhook events |
| Stripe | [`stripe-webhooks`](skills/stripe-webhooks/) | Verify Stripe webhook signatures, parse payment event payloads, handle checkout.session.completed events |
| Vercel | [`vercel-webhooks`](skills/vercel-webhooks/) | Verify Vercel webhook signatures (HMAC-SHA1), handle deployment and project events |
+| WooCommerce | [`woocommerce-webhooks`](skills/woocommerce-webhooks/) | Verify WooCommerce webhook signatures, handle order, product, and customer events |
### Webhook Handler Pattern Skills
diff --git a/providers.yaml b/providers.yaml
index ec621c0..f01ccc2 100644
--- a/providers.yaml
+++ b/providers.yaml
@@ -260,6 +260,22 @@ providers:
- deployment.created
- deployment.succeeded
+ - name: woocommerce
+ displayName: WooCommerce
+ docs:
+ webhooks: https://developer.woocommerce.com/docs/webhooks/
+ events: https://developer.woocommerce.com/docs/webhooks/#webhook-topics
+ notes: >
+ E-commerce platform for WordPress. Uses X-WC-Webhook-Signature header with HMAC-SHA256
+ (base64 encoded). Secret is configured per webhook in WooCommerce admin. Additional headers
+ include X-WC-Webhook-Topic, X-WC-Webhook-Resource, X-WC-Webhook-Event, X-WC-Webhook-Source,
+ X-WC-Webhook-ID, and X-WC-Webhook-Delivery-ID. Common events: order.created, order.updated,
+ product.created, customer.created.
+ testScenario:
+ events:
+ - order.created
+ - order.updated
+
- name: hookdeck-event-gateway
displayName: Hookdeck Event Gateway
docs:
diff --git a/skills/woocommerce-webhooks/SKILL.md b/skills/woocommerce-webhooks/SKILL.md
new file mode 100644
index 0000000..1c9b550
--- /dev/null
+++ b/skills/woocommerce-webhooks/SKILL.md
@@ -0,0 +1,211 @@
+---
+name: woocommerce-webhooks
+description: >
+ Receive and verify WooCommerce webhooks. Use when setting up WooCommerce webhook
+ handlers, debugging signature verification, or handling e-commerce events like
+ order.created, order.updated, product.created, or customer.created.
+license: MIT
+metadata:
+ author: hookdeck
+ version: "0.1.0"
+ repository: https://github.com/hookdeck/webhook-skills
+---
+
+# WooCommerce Webhooks
+
+## When to Use This Skill
+
+- Setting up WooCommerce webhook handlers
+- Debugging signature verification failures
+- Understanding WooCommerce event types and payloads
+- Handling order, product, or customer events
+- Integrating with WooCommerce stores
+
+## Essential Code (USE THIS)
+
+### WooCommerce Signature Verification (JavaScript)
+
+```javascript
+const crypto = require('crypto');
+
+function verifyWooCommerceWebhook(rawBody, signature, secret) {
+ if (!signature || !secret) return false;
+
+ const hash = crypto
+ .createHmac('sha256', secret)
+ .update(rawBody)
+ .digest('base64');
+
+ try {
+ return crypto.timingSafeEqual(
+ Buffer.from(signature),
+ Buffer.from(hash)
+ );
+ } catch {
+ return false;
+ }
+}
+```
+
+### Express Webhook Handler
+
+```javascript
+const express = require('express');
+const app = express();
+
+// CRITICAL: Use raw body for signature verification
+app.use('/webhooks/woocommerce', express.raw({ type: 'application/json' }));
+
+app.post('/webhooks/woocommerce', (req, res) => {
+ const signature = req.headers['x-wc-webhook-signature'];
+ const secret = process.env.WOOCOMMERCE_WEBHOOK_SECRET;
+
+ if (!verifyWooCommerceWebhook(req.body, signature, secret)) {
+ return res.status(400).send('Invalid signature');
+ }
+
+ const payload = JSON.parse(req.body);
+ const topic = req.headers['x-wc-webhook-topic'];
+
+ console.log(`Received ${topic} event:`, payload.id);
+ res.status(200).send('OK');
+});
+```
+
+### Next.js API Route (App Router)
+
+```typescript
+import crypto from 'crypto';
+import { NextRequest } from 'next/server';
+
+export async function POST(request: NextRequest) {
+ const signature = request.headers.get('x-wc-webhook-signature');
+ const secret = process.env.WOOCOMMERCE_WEBHOOK_SECRET;
+
+ const rawBody = await request.text();
+
+ if (!verifyWooCommerceWebhook(rawBody, signature, secret)) {
+ return new Response('Invalid signature', { status: 400 });
+ }
+
+ const payload = JSON.parse(rawBody);
+ const topic = request.headers.get('x-wc-webhook-topic');
+
+ console.log(`Received ${topic} event:`, payload.id);
+ return new Response('OK', { status: 200 });
+}
+```
+
+### FastAPI Handler
+
+```python
+import hmac
+import hashlib
+import base64
+from fastapi import FastAPI, Request, HTTPException
+
+app = FastAPI()
+
+def verify_woocommerce_webhook(raw_body: bytes, signature: str, secret: str) -> bool:
+ if not signature or not secret:
+ return False
+
+ hash_digest = hmac.new(
+ secret.encode(),
+ raw_body,
+ hashlib.sha256
+ ).digest()
+ expected_signature = base64.b64encode(hash_digest).decode()
+
+ return hmac.compare_digest(signature, expected_signature)
+
+@app.post('/webhooks/woocommerce')
+async def handle_webhook(request: Request):
+ raw_body = await request.body()
+ signature = request.headers.get('x-wc-webhook-signature')
+ secret = os.getenv('WOOCOMMERCE_WEBHOOK_SECRET')
+
+ if not verify_woocommerce_webhook(raw_body, signature, secret):
+ raise HTTPException(status_code=400, detail='Invalid signature')
+
+ payload = await request.json()
+ topic = request.headers.get('x-wc-webhook-topic')
+
+ print(f"Received {topic} event: {payload.get('id')}")
+ return {'status': 'success'}
+```
+
+## Common Event Types
+
+| Event | Triggered When | Common Use Cases |
+|-------|----------------|------------------|
+| `order.created` | New order placed | Send confirmation emails, update inventory |
+| `order.updated` | Order status changed | Track fulfillment, send notifications |
+| `order.deleted` | Order deleted | Clean up external systems |
+| `product.created` | Product added | Sync to external catalogs |
+| `product.updated` | Product modified | Update pricing, inventory |
+| `customer.created` | New customer registered | Welcome emails, CRM sync |
+| `customer.updated` | Customer info changed | Update profiles, preferences |
+
+## Environment Variables
+
+```bash
+WOOCOMMERCE_WEBHOOK_SECRET=your_webhook_secret_key
+```
+
+## Headers Reference
+
+WooCommerce webhooks include these headers:
+
+- `X-WC-Webhook-Signature` - HMAC SHA256 signature (base64)
+- `X-WC-Webhook-Topic` - Event type (e.g., "order.created")
+- `X-WC-Webhook-Resource` - Resource type (e.g., "order")
+- `X-WC-Webhook-Event` - Action (e.g., "created")
+- `X-WC-Webhook-Source` - Store URL
+- `X-WC-Webhook-ID` - Webhook ID
+- `X-WC-Webhook-Delivery-ID` - Unique delivery ID
+
+## Local Development
+
+For local webhook testing, install Hookdeck CLI:
+
+```bash
+# Install via npm
+npm install -g hookdeck-cli
+
+# Or via Homebrew
+brew install hookdeck/hookdeck/hookdeck
+```
+
+Then start the tunnel:
+
+```bash
+hookdeck listen 3000 --path /webhooks/woocommerce
+```
+
+No account required. Provides local tunnel + web UI for inspecting requests.
+
+## Reference Materials
+
+- `overview.md` - What WooCommerce webhooks are, common event types
+- `setup.md` - Configure webhooks in WooCommerce admin, get signing secret
+- `verification.md` - Signature verification details and gotchas
+- `examples/` - Complete runnable examples per framework
+
+## Recommended: webhook-handler-patterns
+
+For production-ready webhook handlers, also install the webhook-handler-patterns skill for:
+
+- Handler sequence
+- Idempotency
+- Error handling
+- Retry logic
+
+## Related Skills
+
+- [stripe-webhooks](https://github.com/hookdeck/webhook-skills/tree/main/skills/stripe-webhooks) - Stripe payment webhooks with HMAC verification
+- [shopify-webhooks](https://github.com/hookdeck/webhook-skills/tree/main/skills/shopify-webhooks) - Shopify store webhooks with HMAC verification
+- [github-webhooks](https://github.com/hookdeck/webhook-skills/tree/main/skills/github-webhooks) - GitHub repository webhooks
+- [paddle-webhooks](https://github.com/hookdeck/webhook-skills/tree/main/skills/paddle-webhooks) - Paddle billing webhooks
+- [webhook-handler-patterns](https://github.com/hookdeck/webhook-skills/tree/main/skills/webhook-handler-patterns) - Idempotency, error handling, retry logic
+- [hookdeck-event-gateway](https://github.com/hookdeck/webhook-skills/tree/main/skills/hookdeck-event-gateway) - Production webhook infrastructure
\ No newline at end of file
diff --git a/skills/woocommerce-webhooks/examples/express/.env.example b/skills/woocommerce-webhooks/examples/express/.env.example
new file mode 100644
index 0000000..7af6dec
--- /dev/null
+++ b/skills/woocommerce-webhooks/examples/express/.env.example
@@ -0,0 +1 @@
+WOOCOMMERCE_WEBHOOK_SECRET=your_webhook_secret_key_here
\ No newline at end of file
diff --git a/skills/woocommerce-webhooks/examples/express/README.md b/skills/woocommerce-webhooks/examples/express/README.md
new file mode 100644
index 0000000..7a41f7e
--- /dev/null
+++ b/skills/woocommerce-webhooks/examples/express/README.md
@@ -0,0 +1,54 @@
+# WooCommerce Webhooks - Express Example
+
+Minimal example of receiving WooCommerce webhooks with signature verification.
+
+## Prerequisites
+
+- Node.js 18+
+- WooCommerce store with webhook signing secret
+
+## Setup
+
+1. Install dependencies:
+ ```bash
+ npm install
+ ```
+
+2. Copy environment variables:
+ ```bash
+ cp .env.example .env
+ ```
+
+3. Add your WooCommerce webhook secret to `.env`:
+ ```
+ WOOCOMMERCE_WEBHOOK_SECRET=your_webhook_secret_from_woocommerce
+ ```
+
+## Run
+
+```bash
+npm start
+```
+
+Server runs on http://localhost:3000
+
+## Test
+
+You can test with a sample payload:
+
+```bash
+curl -X POST http://localhost:3000/webhooks/woocommerce \
+ -H "Content-Type: application/json" \
+ -H "X-WC-Webhook-Topic: order.created" \
+ -H "X-WC-Webhook-Signature: your_generated_signature" \
+ -d '{"id": 123, "status": "processing", "total": "29.99"}'
+```
+
+The signature should be generated using HMAC SHA-256 with your webhook secret.
+
+## WooCommerce Setup
+
+In your WooCommerce admin:
+1. Go to **WooCommerce > Settings > Advanced > Webhooks**
+2. Add webhook with delivery URL: `http://localhost:3000/webhooks/woocommerce`
+3. Copy the webhook secret to your `.env` file
\ No newline at end of file
diff --git a/skills/woocommerce-webhooks/examples/express/package.json b/skills/woocommerce-webhooks/examples/express/package.json
new file mode 100644
index 0000000..855fa4d
--- /dev/null
+++ b/skills/woocommerce-webhooks/examples/express/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "woocommerce-webhook-express",
+ "version": "1.0.0",
+ "description": "WooCommerce webhook handler using Express",
+ "main": "src/index.js",
+ "scripts": {
+ "start": "node src/index.js",
+ "test": "jest",
+ "dev": "nodemon src/index.js"
+ },
+ "dependencies": {
+ "express": "^5.2.1",
+ "dotenv": "^16.4.7"
+ },
+ "devDependencies": {
+ "jest": "^30.2.0",
+ "nodemon": "^3.1.9",
+ "supertest": "^7.0.0"
+ },
+ "jest": {
+ "testEnvironment": "node"
+ }
+}
\ No newline at end of file
diff --git a/skills/woocommerce-webhooks/examples/express/src/index.js b/skills/woocommerce-webhooks/examples/express/src/index.js
new file mode 100644
index 0000000..47eb7b4
--- /dev/null
+++ b/skills/woocommerce-webhooks/examples/express/src/index.js
@@ -0,0 +1,130 @@
+const express = require('express');
+const crypto = require('crypto');
+require('dotenv').config();
+
+const app = express();
+const PORT = process.env.PORT || 3000;
+
+/**
+ * Verify WooCommerce webhook signature using HMAC SHA-256
+ * @param {Buffer} rawBody - Raw request body
+ * @param {string} signature - X-WC-Webhook-Signature header value
+ * @param {string} secret - Webhook secret from WooCommerce
+ * @returns {boolean} - True if signature is valid
+ */
+function verifyWooCommerceWebhook(rawBody, signature, secret) {
+ if (!signature || !secret || !rawBody) {
+ return false;
+ }
+
+ const expectedSignature = crypto
+ .createHmac('sha256', secret)
+ .update(rawBody)
+ .digest('base64');
+
+ try {
+ return crypto.timingSafeEqual(
+ Buffer.from(signature),
+ Buffer.from(expectedSignature)
+ );
+ } catch (error) {
+ // Different lengths will cause an error
+ return false;
+ }
+}
+
+/**
+ * Handle different WooCommerce event types
+ * @param {string} topic - Event topic (e.g., "order.created")
+ * @param {Object} payload - Webhook payload
+ */
+function handleWooCommerceEvent(topic, payload) {
+ console.log(`Processing ${topic} event for ID: ${payload.id}`);
+
+ switch (topic) {
+ case 'order.created':
+ console.log(`New order #${payload.id} for $${payload.total}`);
+ // Add your order processing logic here
+ break;
+
+ case 'order.updated':
+ console.log(`Order #${payload.id} updated to status: ${payload.status}`);
+ // Add your order update logic here
+ break;
+
+ case 'product.created':
+ console.log(`New product: ${payload.name} (ID: ${payload.id})`);
+ // Add your product sync logic here
+ break;
+
+ case 'product.updated':
+ console.log(`Product updated: ${payload.name} (ID: ${payload.id})`);
+ // Add your product update logic here
+ break;
+
+ case 'customer.created':
+ console.log(`New customer: ${payload.email} (ID: ${payload.id})`);
+ // Add your customer onboarding logic here
+ break;
+
+ case 'customer.updated':
+ console.log(`Customer updated: ${payload.email} (ID: ${payload.id})`);
+ // Add your customer update logic here
+ break;
+
+ default:
+ console.log(`Unhandled event type: ${topic}`);
+ }
+}
+
+// CRITICAL: Use raw body parser for signature verification
+app.use('/webhooks/woocommerce', express.raw({ type: 'application/json' }));
+
+// WooCommerce webhook endpoint
+app.post('/webhooks/woocommerce', (req, res) => {
+ try {
+ const signature = req.headers['x-wc-webhook-signature'];
+ const topic = req.headers['x-wc-webhook-topic'];
+ const source = req.headers['x-wc-webhook-source'];
+ const secret = process.env.WOOCOMMERCE_WEBHOOK_SECRET;
+
+ console.log(`Received webhook: ${topic} from ${source}`);
+
+ // Verify webhook signature
+ if (!verifyWooCommerceWebhook(req.body, signature, secret)) {
+ console.log('❌ Invalid webhook signature');
+ return res.status(400).json({ error: 'Invalid signature' });
+ }
+
+ console.log('✅ Signature verified');
+
+ // Parse the JSON payload
+ const payload = JSON.parse(req.body.toString());
+
+ // Handle the event
+ handleWooCommerceEvent(topic, payload);
+
+ // Respond with success
+ res.status(200).json({ received: true });
+
+ } catch (error) {
+ console.error('Error processing webhook:', error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+});
+
+// Health check endpoint
+app.get('/health', (req, res) => {
+ res.status(200).json({ status: 'healthy', timestamp: new Date().toISOString() });
+});
+
+// Start server only when run directly (not when imported for testing)
+if (require.main === module) {
+ app.listen(PORT, () => {
+ console.log(`🚀 WooCommerce webhook server running on port ${PORT}`);
+ console.log(`📍 Webhook endpoint: http://localhost:${PORT}/webhooks/woocommerce`);
+ console.log('🔒 Make sure to set WOOCOMMERCE_WEBHOOK_SECRET in your environment');
+ });
+}
+
+module.exports = { app, verifyWooCommerceWebhook };
\ No newline at end of file
diff --git a/skills/woocommerce-webhooks/examples/express/test/webhook.test.js b/skills/woocommerce-webhooks/examples/express/test/webhook.test.js
new file mode 100644
index 0000000..9070ea6
--- /dev/null
+++ b/skills/woocommerce-webhooks/examples/express/test/webhook.test.js
@@ -0,0 +1,211 @@
+const request = require('supertest');
+const crypto = require('crypto');
+const { app, verifyWooCommerceWebhook } = require('../src/index');
+
+// Test webhook secret
+const TEST_SECRET = 'test_woocommerce_secret_key';
+
+// Set test environment variable
+process.env.WOOCOMMERCE_WEBHOOK_SECRET = TEST_SECRET;
+
+/**
+ * Generate a valid WooCommerce webhook signature for testing
+ * @param {string} payload - JSON payload as string
+ * @param {string} secret - Webhook secret
+ * @returns {string} - Base64 encoded signature
+ */
+function generateTestSignature(payload, secret) {
+ return crypto
+ .createHmac('sha256', secret)
+ .update(payload)
+ .digest('base64');
+}
+
+describe('WooCommerce Webhook Handler', () => {
+ describe('Signature Verification', () => {
+ test('should verify valid signatures', () => {
+ const payload = '{"id": 123, "status": "processing"}';
+ const signature = generateTestSignature(payload, TEST_SECRET);
+
+ const isValid = verifyWooCommerceWebhook(
+ Buffer.from(payload),
+ signature,
+ TEST_SECRET
+ );
+
+ expect(isValid).toBe(true);
+ });
+
+ test('should reject invalid signatures', () => {
+ const payload = '{"id": 123, "status": "processing"}';
+ const invalidSignature = 'invalid_signature';
+
+ const isValid = verifyWooCommerceWebhook(
+ Buffer.from(payload),
+ invalidSignature,
+ TEST_SECRET
+ );
+
+ expect(isValid).toBe(false);
+ });
+
+ test('should reject missing signature', () => {
+ const payload = '{"id": 123, "status": "processing"}';
+
+ const isValid = verifyWooCommerceWebhook(
+ Buffer.from(payload),
+ null,
+ TEST_SECRET
+ );
+
+ expect(isValid).toBe(false);
+ });
+
+ test('should reject missing secret', () => {
+ const payload = '{"id": 123, "status": "processing"}';
+ const signature = generateTestSignature(payload, TEST_SECRET);
+
+ const isValid = verifyWooCommerceWebhook(
+ Buffer.from(payload),
+ signature,
+ null
+ );
+
+ expect(isValid).toBe(false);
+ });
+
+ test('should handle different payload lengths', () => {
+ const payloads = [
+ '{}',
+ '{"id":1}',
+ '{"id": 123, "status": "processing", "total": "29.99", "customer": {"name": "John Doe"}}'
+ ];
+
+ payloads.forEach(payload => {
+ const signature = generateTestSignature(payload, TEST_SECRET);
+ const isValid = verifyWooCommerceWebhook(
+ Buffer.from(payload),
+ signature,
+ TEST_SECRET
+ );
+ expect(isValid).toBe(true);
+ });
+ });
+ });
+
+ describe('Webhook Endpoint', () => {
+ test('should accept valid order.created webhook', async () => {
+ const payload = {
+ id: 123,
+ status: 'processing',
+ total: '29.99',
+ currency: 'USD',
+ billing: {
+ first_name: 'John',
+ last_name: 'Doe',
+ email: 'john@example.com'
+ }
+ };
+
+ const payloadString = JSON.stringify(payload);
+ const signature = generateTestSignature(payloadString, TEST_SECRET);
+
+ const response = await request(app)
+ .post('/webhooks/woocommerce')
+ .set('Content-Type', 'application/json')
+ .set('X-WC-Webhook-Topic', 'order.created')
+ .set('X-WC-Webhook-Signature', signature)
+ .set('X-WC-Webhook-Source', 'https://example.com')
+ .send(payloadString);
+
+ expect(response.status).toBe(200);
+ expect(response.body).toEqual({ received: true });
+ });
+
+ test('should accept valid product.updated webhook', async () => {
+ const payload = {
+ id: 456,
+ name: 'Premium T-Shirt',
+ status: 'publish',
+ regular_price: '29.99',
+ stock_status: 'instock'
+ };
+
+ const payloadString = JSON.stringify(payload);
+ const signature = generateTestSignature(payloadString, TEST_SECRET);
+
+ const response = await request(app)
+ .post('/webhooks/woocommerce')
+ .set('Content-Type', 'application/json')
+ .set('X-WC-Webhook-Topic', 'product.updated')
+ .set('X-WC-Webhook-Signature', signature)
+ .set('X-WC-Webhook-Source', 'https://example.com')
+ .send(payloadString);
+
+ expect(response.status).toBe(200);
+ expect(response.body).toEqual({ received: true });
+ });
+
+ test('should reject webhook with invalid signature', async () => {
+ const payload = {
+ id: 123,
+ status: 'processing'
+ };
+
+ const response = await request(app)
+ .post('/webhooks/woocommerce')
+ .set('Content-Type', 'application/json')
+ .set('X-WC-Webhook-Topic', 'order.created')
+ .set('X-WC-Webhook-Signature', 'invalid_signature')
+ .set('X-WC-Webhook-Source', 'https://example.com')
+ .send(JSON.stringify(payload));
+
+ expect(response.status).toBe(400);
+ expect(response.body).toEqual({ error: 'Invalid signature' });
+ });
+
+ test('should reject webhook without signature header', async () => {
+ const payload = {
+ id: 123,
+ status: 'processing'
+ };
+
+ const response = await request(app)
+ .post('/webhooks/woocommerce')
+ .set('Content-Type', 'application/json')
+ .set('X-WC-Webhook-Topic', 'order.created')
+ .set('X-WC-Webhook-Source', 'https://example.com')
+ .send(JSON.stringify(payload));
+
+ expect(response.status).toBe(400);
+ expect(response.body).toEqual({ error: 'Invalid signature' });
+ });
+
+ test('should handle malformed JSON gracefully', async () => {
+ const invalidPayload = '{"id": 123, "status":}'; // Invalid JSON
+ const signature = generateTestSignature(invalidPayload, TEST_SECRET);
+
+ const response = await request(app)
+ .post('/webhooks/woocommerce')
+ .set('Content-Type', 'application/json')
+ .set('X-WC-Webhook-Topic', 'order.created')
+ .set('X-WC-Webhook-Signature', signature)
+ .set('X-WC-Webhook-Source', 'https://example.com')
+ .send(invalidPayload);
+
+ expect(response.status).toBe(500);
+ expect(response.body).toEqual({ error: 'Internal server error' });
+ });
+ });
+
+ describe('Health Check', () => {
+ test('should return healthy status', async () => {
+ const response = await request(app)
+ .get('/health');
+
+ expect(response.status).toBe(200);
+ expect(response.body.status).toBe('healthy');
+ expect(response.body.timestamp).toBeDefined();
+ });
+ });
+});
\ No newline at end of file
diff --git a/skills/woocommerce-webhooks/examples/fastapi/.env.example b/skills/woocommerce-webhooks/examples/fastapi/.env.example
new file mode 100644
index 0000000..7af6dec
--- /dev/null
+++ b/skills/woocommerce-webhooks/examples/fastapi/.env.example
@@ -0,0 +1 @@
+WOOCOMMERCE_WEBHOOK_SECRET=your_webhook_secret_key_here
\ No newline at end of file
diff --git a/skills/woocommerce-webhooks/examples/fastapi/README.md b/skills/woocommerce-webhooks/examples/fastapi/README.md
new file mode 100644
index 0000000..a1ce78b
--- /dev/null
+++ b/skills/woocommerce-webhooks/examples/fastapi/README.md
@@ -0,0 +1,69 @@
+# WooCommerce Webhooks - FastAPI Example
+
+Minimal example of receiving WooCommerce webhooks with signature verification using FastAPI.
+
+## Prerequisites
+
+- Python 3.9+
+- WooCommerce store with webhook signing secret
+
+## Setup
+
+1. Create a virtual environment:
+ ```bash
+ python3 -m venv venv
+ source venv/bin/activate # On Windows: venv\Scripts\activate
+ ```
+
+2. Install dependencies:
+ ```bash
+ pip install -r requirements.txt
+ ```
+
+3. Copy environment variables:
+ ```bash
+ cp .env.example .env
+ ```
+
+4. Add your WooCommerce webhook secret to `.env`:
+ ```
+ WOOCOMMERCE_WEBHOOK_SECRET=your_webhook_secret_from_woocommerce
+ ```
+
+## Run
+
+Development mode:
+```bash
+uvicorn main:app --reload
+```
+
+Production mode:
+```bash
+uvicorn main:app --host 0.0.0.0 --port 8000
+```
+
+Server runs on http://localhost:8000
+
+## Test
+
+You can test with a sample payload:
+
+```bash
+curl -X POST http://localhost:8000/webhooks/woocommerce \
+ -H "Content-Type: application/json" \
+ -H "X-WC-Webhook-Topic: order.created" \
+ -H "X-WC-Webhook-Signature: your_generated_signature" \
+ -d '{"id": 123, "status": "processing", "total": "29.99"}'
+```
+
+Run tests:
+```bash
+pytest test_webhook.py -v
+```
+
+## WooCommerce Setup
+
+In your WooCommerce admin:
+1. Go to **WooCommerce > Settings > Advanced > Webhooks**
+2. Add webhook with delivery URL: `http://localhost:8000/webhooks/woocommerce`
+3. Copy the webhook secret to your `.env` file
\ No newline at end of file
diff --git a/skills/woocommerce-webhooks/examples/fastapi/main.py b/skills/woocommerce-webhooks/examples/fastapi/main.py
new file mode 100644
index 0000000..6700202
--- /dev/null
+++ b/skills/woocommerce-webhooks/examples/fastapi/main.py
@@ -0,0 +1,131 @@
+import os
+import hmac
+import hashlib
+import base64
+from typing import Any, Dict, Optional
+from dotenv import load_dotenv
+from fastapi import FastAPI, Request, HTTPException
+
+# Load environment variables
+load_dotenv()
+
+app = FastAPI(title="WooCommerce Webhook Handler", version="1.0.0")
+
+def verify_woocommerce_webhook(raw_body: bytes, signature: Optional[str], secret: Optional[str]) -> bool:
+ """
+ Verify WooCommerce webhook signature using HMAC SHA-256
+
+ Args:
+ raw_body: Raw request body as bytes
+ signature: X-WC-Webhook-Signature header value
+ secret: Webhook secret from WooCommerce
+
+ Returns:
+ True if signature is valid
+ """
+ if not signature or not secret or not raw_body:
+ return False
+
+ # Generate expected signature
+ hash_digest = hmac.new(
+ secret.encode('utf-8'),
+ raw_body,
+ hashlib.sha256
+ ).digest()
+
+ expected_signature = base64.b64encode(hash_digest).decode('utf-8')
+
+ # Use timing-safe comparison to prevent timing attacks
+ return hmac.compare_digest(signature, expected_signature)
+
+def handle_woocommerce_event(topic: str, payload: Dict[str, Any]) -> None:
+ """
+ Handle different WooCommerce event types
+
+ Args:
+ topic: Event topic (e.g., "order.created")
+ payload: Webhook payload
+ """
+ print(f"Processing {topic} event for ID: {payload.get('id')}")
+
+ if topic == 'order.created':
+ print(f"New order #{payload.get('id')} for ${payload.get('total')}")
+ # Add your order processing logic here
+
+ elif topic == 'order.updated':
+ print(f"Order #{payload.get('id')} updated to status: {payload.get('status')}")
+ # Add your order update logic here
+
+ elif topic == 'product.created':
+ print(f"New product: {payload.get('name')} (ID: {payload.get('id')})")
+ # Add your product sync logic here
+
+ elif topic == 'product.updated':
+ print(f"Product updated: {payload.get('name')} (ID: {payload.get('id')})")
+ # Add your product update logic here
+
+ elif topic == 'customer.created':
+ print(f"New customer: {payload.get('email')} (ID: {payload.get('id')})")
+ # Add your customer onboarding logic here
+
+ elif topic == 'customer.updated':
+ print(f"Customer updated: {payload.get('email')} (ID: {payload.get('id')})")
+ # Add your customer update logic here
+
+ else:
+ print(f"Unhandled event type: {topic}")
+
+@app.post("/webhooks/woocommerce")
+async def handle_webhook(request: Request):
+ """
+ WooCommerce webhook endpoint
+ """
+ try:
+ # Get headers
+ signature = request.headers.get('x-wc-webhook-signature')
+ topic = request.headers.get('x-wc-webhook-topic')
+ source = request.headers.get('x-wc-webhook-source')
+ secret = os.getenv('WOOCOMMERCE_WEBHOOK_SECRET')
+
+ print(f"Received webhook: {topic} from {source}")
+
+ # Get raw body for signature verification
+ raw_body = await request.body()
+
+ # Verify webhook signature
+ if not verify_woocommerce_webhook(raw_body, signature, secret):
+ print("❌ Invalid webhook signature")
+ raise HTTPException(status_code=400, detail="Invalid signature")
+
+ print("✅ Signature verified")
+
+ # Parse the JSON payload
+ payload = await request.json()
+
+ # Handle the event
+ if topic:
+ handle_woocommerce_event(topic, payload)
+
+ # Respond with success
+ return {"received": True}
+
+ except HTTPException:
+ # Re-raise HTTP exceptions (like 400 for invalid signature)
+ raise
+ except Exception as e:
+ print(f"Error processing webhook: {e}")
+ raise HTTPException(status_code=500, detail="Internal server error")
+
+@app.get("/health")
+async def health_check():
+ """
+ Health check endpoint
+ """
+ return {
+ "status": "healthy",
+ "timestamp": "2024-01-15T10:30:00Z"
+ }
+
+if __name__ == "__main__":
+ import uvicorn
+ uvicorn.run(app, host="0.0.0.0", port=8000)
\ No newline at end of file
diff --git a/skills/woocommerce-webhooks/examples/fastapi/requirements.txt b/skills/woocommerce-webhooks/examples/fastapi/requirements.txt
new file mode 100644
index 0000000..44107fc
--- /dev/null
+++ b/skills/woocommerce-webhooks/examples/fastapi/requirements.txt
@@ -0,0 +1,4 @@
+fastapi>=0.128.1
+python-dotenv>=1.0.0
+pytest>=9.0.2
+httpx>=0.28.1
\ No newline at end of file
diff --git a/skills/woocommerce-webhooks/examples/fastapi/test_webhook.py b/skills/woocommerce-webhooks/examples/fastapi/test_webhook.py
new file mode 100644
index 0000000..69e1fe0
--- /dev/null
+++ b/skills/woocommerce-webhooks/examples/fastapi/test_webhook.py
@@ -0,0 +1,210 @@
+import pytest
+import os
+import json
+import hmac
+import hashlib
+import base64
+from fastapi.testclient import TestClient
+from main import app, verify_woocommerce_webhook
+
+# Test webhook secret
+TEST_SECRET = 'test_woocommerce_secret_key'
+
+# Set test environment variable
+os.environ['WOOCOMMERCE_WEBHOOK_SECRET'] = TEST_SECRET
+
+client = TestClient(app)
+
+def generate_test_signature(payload: str, secret: str) -> str:
+ """
+ Generate a valid WooCommerce webhook signature for testing
+
+ Args:
+ payload: JSON payload as string
+ secret: Webhook secret
+
+ Returns:
+ Base64 encoded signature
+ """
+ hash_digest = hmac.new(
+ secret.encode('utf-8'),
+ payload.encode('utf-8'),
+ hashlib.sha256
+ ).digest()
+ return base64.b64encode(hash_digest).decode('utf-8')
+
+class TestSignatureVerification:
+ def test_should_verify_valid_signatures(self):
+ payload = '{"id": 123, "status": "processing"}'
+ signature = generate_test_signature(payload, TEST_SECRET)
+
+ is_valid = verify_woocommerce_webhook(
+ payload.encode('utf-8'),
+ signature,
+ TEST_SECRET
+ )
+
+ assert is_valid is True
+
+ def test_should_reject_invalid_signatures(self):
+ payload = '{"id": 123, "status": "processing"}'
+ invalid_signature = 'invalid_signature'
+
+ is_valid = verify_woocommerce_webhook(
+ payload.encode('utf-8'),
+ invalid_signature,
+ TEST_SECRET
+ )
+
+ assert is_valid is False
+
+ def test_should_reject_missing_signature(self):
+ payload = '{"id": 123, "status": "processing"}'
+
+ is_valid = verify_woocommerce_webhook(
+ payload.encode('utf-8'),
+ None,
+ TEST_SECRET
+ )
+
+ assert is_valid is False
+
+ def test_should_reject_missing_secret(self):
+ payload = '{"id": 123, "status": "processing"}'
+ signature = generate_test_signature(payload, TEST_SECRET)
+
+ is_valid = verify_woocommerce_webhook(
+ payload.encode('utf-8'),
+ signature,
+ None
+ )
+
+ assert is_valid is False
+
+ def test_should_handle_different_payload_lengths(self):
+ payloads = [
+ '{}',
+ '{"id":1}',
+ '{"id": 123, "status": "processing", "total": "29.99", "customer": {"name": "John Doe"}}'
+ ]
+
+ for payload in payloads:
+ signature = generate_test_signature(payload, TEST_SECRET)
+ is_valid = verify_woocommerce_webhook(
+ payload.encode('utf-8'),
+ signature,
+ TEST_SECRET
+ )
+ assert is_valid is True
+
+class TestWebhookEndpoint:
+ def test_should_accept_valid_order_created_webhook(self):
+ payload = {
+ "id": 123,
+ "status": "processing",
+ "total": "29.99",
+ "currency": "USD",
+ "billing": {
+ "first_name": "John",
+ "last_name": "Doe",
+ "email": "john@example.com"
+ }
+ }
+
+ # Convert to JSON string for signature generation
+ payload_string = json.dumps(payload, separators=(',', ':'))
+ signature = generate_test_signature(payload_string, TEST_SECRET)
+
+ # Send the raw JSON string, not using json= parameter
+ response = client.post(
+ "/webhooks/woocommerce",
+ content=payload_string,
+ headers={
+ "Content-Type": "application/json",
+ "X-WC-Webhook-Topic": "order.created",
+ "X-WC-Webhook-Signature": signature,
+ "X-WC-Webhook-Source": "https://example.com"
+ }
+ )
+
+ assert response.status_code == 200
+ assert response.json() == {"received": True}
+
+ def test_should_accept_valid_product_updated_webhook(self):
+ payload = {
+ "id": 456,
+ "name": "Premium T-Shirt",
+ "status": "publish",
+ "regular_price": "29.99",
+ "stock_status": "instock"
+ }
+
+ # Convert to JSON string for signature generation
+ payload_string = json.dumps(payload, separators=(',', ':'))
+ signature = generate_test_signature(payload_string, TEST_SECRET)
+
+ # Send the raw JSON string, not using json= parameter
+ response = client.post(
+ "/webhooks/woocommerce",
+ content=payload_string,
+ headers={
+ "Content-Type": "application/json",
+ "X-WC-Webhook-Topic": "product.updated",
+ "X-WC-Webhook-Signature": signature,
+ "X-WC-Webhook-Source": "https://example.com"
+ }
+ )
+
+ assert response.status_code == 200
+ assert response.json() == {"received": True}
+
+ def test_should_reject_webhook_with_invalid_signature(self):
+ payload = {
+ "id": 123,
+ "status": "processing"
+ }
+
+ payload_string = json.dumps(payload, separators=(',', ':'))
+
+ response = client.post(
+ "/webhooks/woocommerce",
+ content=payload_string,
+ headers={
+ "Content-Type": "application/json",
+ "X-WC-Webhook-Topic": "order.created",
+ "X-WC-Webhook-Signature": "invalid_signature",
+ "X-WC-Webhook-Source": "https://example.com"
+ }
+ )
+
+ assert response.status_code == 400
+ assert response.json() == {"detail": "Invalid signature"}
+
+ def test_should_reject_webhook_without_signature_header(self):
+ payload = {
+ "id": 123,
+ "status": "processing"
+ }
+
+ payload_string = json.dumps(payload, separators=(',', ':'))
+
+ response = client.post(
+ "/webhooks/woocommerce",
+ content=payload_string,
+ headers={
+ "Content-Type": "application/json",
+ "X-WC-Webhook-Topic": "order.created",
+ "X-WC-Webhook-Source": "https://example.com"
+ }
+ )
+
+ assert response.status_code == 400
+ assert response.json() == {"detail": "Invalid signature"}
+
+class TestHealthCheck:
+ def test_should_return_healthy_status(self):
+ response = client.get("/health")
+
+ assert response.status_code == 200
+ assert response.json()["status"] == "healthy"
+ assert "timestamp" in response.json()
\ No newline at end of file
diff --git a/skills/woocommerce-webhooks/examples/nextjs/.env.example b/skills/woocommerce-webhooks/examples/nextjs/.env.example
new file mode 100644
index 0000000..7af6dec
--- /dev/null
+++ b/skills/woocommerce-webhooks/examples/nextjs/.env.example
@@ -0,0 +1 @@
+WOOCOMMERCE_WEBHOOK_SECRET=your_webhook_secret_key_here
\ No newline at end of file
diff --git a/skills/woocommerce-webhooks/examples/nextjs/README.md b/skills/woocommerce-webhooks/examples/nextjs/README.md
new file mode 100644
index 0000000..79ed8e1
--- /dev/null
+++ b/skills/woocommerce-webhooks/examples/nextjs/README.md
@@ -0,0 +1,59 @@
+# WooCommerce Webhooks - Next.js Example
+
+Minimal example of receiving WooCommerce webhooks with signature verification using Next.js App Router.
+
+## Prerequisites
+
+- Node.js 18+
+- WooCommerce store with webhook signing secret
+
+## Setup
+
+1. Install dependencies:
+ ```bash
+ npm install
+ ```
+
+2. Copy environment variables:
+ ```bash
+ cp .env.example .env.local
+ ```
+
+3. Add your WooCommerce webhook secret to `.env.local`:
+ ```
+ WOOCOMMERCE_WEBHOOK_SECRET=your_webhook_secret_from_woocommerce
+ ```
+
+## Run
+
+Development mode:
+```bash
+npm run dev
+```
+
+Production build:
+```bash
+npm run build
+npm start
+```
+
+Server runs on http://localhost:3000
+
+## Test
+
+You can test with a sample payload:
+
+```bash
+curl -X POST http://localhost:3000/webhooks/woocommerce \
+ -H "Content-Type: application/json" \
+ -H "X-WC-Webhook-Topic: order.created" \
+ -H "X-WC-Webhook-Signature: your_generated_signature" \
+ -d '{"id": 123, "status": "processing", "total": "29.99"}'
+```
+
+## WooCommerce Setup
+
+In your WooCommerce admin:
+1. Go to **WooCommerce > Settings > Advanced > Webhooks**
+2. Add webhook with delivery URL: `http://localhost:3000/webhooks/woocommerce`
+3. Copy the webhook secret to your `.env.local` file
\ No newline at end of file
diff --git a/skills/woocommerce-webhooks/examples/nextjs/app/webhooks/woocommerce/route.ts b/skills/woocommerce-webhooks/examples/nextjs/app/webhooks/woocommerce/route.ts
new file mode 100644
index 0000000..3cbf2c1
--- /dev/null
+++ b/skills/woocommerce-webhooks/examples/nextjs/app/webhooks/woocommerce/route.ts
@@ -0,0 +1,123 @@
+import crypto from 'crypto';
+import { NextRequest } from 'next/server';
+
+/**
+ * Verify WooCommerce webhook signature using HMAC SHA-256
+ * @param rawBody - Raw request body as string
+ * @param signature - X-WC-Webhook-Signature header value
+ * @param secret - Webhook secret from WooCommerce
+ * @returns True if signature is valid
+ */
+function verifyWooCommerceWebhook(rawBody: string, signature: string | null, secret: string | undefined): boolean {
+ if (!signature || !secret || !rawBody) {
+ return false;
+ }
+
+ const expectedSignature = crypto
+ .createHmac('sha256', secret)
+ .update(rawBody)
+ .digest('base64');
+
+ try {
+ return crypto.timingSafeEqual(
+ Buffer.from(signature),
+ Buffer.from(expectedSignature)
+ );
+ } catch (error) {
+ // Different lengths will cause an error
+ return false;
+ }
+}
+
+/**
+ * Handle different WooCommerce event types
+ * @param topic - Event topic (e.g., "order.created")
+ * @param payload - Webhook payload
+ */
+function handleWooCommerceEvent(topic: string, payload: any) {
+ console.log(`Processing ${topic} event for ID: ${payload.id}`);
+
+ switch (topic) {
+ case 'order.created':
+ console.log(`New order #${payload.id} for $${payload.total}`);
+ // Add your order processing logic here
+ break;
+
+ case 'order.updated':
+ console.log(`Order #${payload.id} updated to status: ${payload.status}`);
+ // Add your order update logic here
+ break;
+
+ case 'product.created':
+ console.log(`New product: ${payload.name} (ID: ${payload.id})`);
+ // Add your product sync logic here
+ break;
+
+ case 'product.updated':
+ console.log(`Product updated: ${payload.name} (ID: ${payload.id})`);
+ // Add your product update logic here
+ break;
+
+ case 'customer.created':
+ console.log(`New customer: ${payload.email} (ID: ${payload.id})`);
+ // Add your customer onboarding logic here
+ break;
+
+ case 'customer.updated':
+ console.log(`Customer updated: ${payload.email} (ID: ${payload.id})`);
+ // Add your customer update logic here
+ break;
+
+ default:
+ console.log(`Unhandled event type: ${topic}`);
+ }
+}
+
+export async function POST(request: NextRequest) {
+ try {
+ const signature = request.headers.get('x-wc-webhook-signature');
+ const topic = request.headers.get('x-wc-webhook-topic');
+ const source = request.headers.get('x-wc-webhook-source');
+ const secret = process.env.WOOCOMMERCE_WEBHOOK_SECRET;
+
+ console.log(`Received webhook: ${topic} from ${source}`);
+
+ // Get raw body for signature verification
+ const rawBody = await request.text();
+
+ // Verify webhook signature
+ if (!verifyWooCommerceWebhook(rawBody, signature, secret)) {
+ console.log('❌ Invalid webhook signature');
+ return new Response(JSON.stringify({ error: 'Invalid signature' }), {
+ status: 400,
+ headers: { 'Content-Type': 'application/json' },
+ });
+ }
+
+ console.log('✅ Signature verified');
+
+ // Parse the JSON payload
+ const payload = JSON.parse(rawBody);
+
+ // Handle the event
+ if (topic) {
+ handleWooCommerceEvent(topic, payload);
+ }
+
+ // Respond with success
+ return new Response(JSON.stringify({ received: true }), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' },
+ });
+
+ } catch (error) {
+ console.error('Error processing webhook:', error);
+ return new Response(JSON.stringify({ error: 'Internal server error' }), {
+ status: 500,
+ headers: { 'Content-Type': 'application/json' },
+ });
+ }
+}
+
+// Export the verification function for testing
+export { verifyWooCommerceWebhook };
\ No newline at end of file
diff --git a/skills/woocommerce-webhooks/examples/nextjs/package.json b/skills/woocommerce-webhooks/examples/nextjs/package.json
new file mode 100644
index 0000000..e3258f8
--- /dev/null
+++ b/skills/woocommerce-webhooks/examples/nextjs/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "woocommerce-webhook-nextjs",
+ "version": "1.0.0",
+ "description": "WooCommerce webhook handler using Next.js App Router",
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "test": "vitest run",
+ "test:watch": "vitest"
+ },
+ "dependencies": {
+ "next": "^16.1.6",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ },
+ "devDependencies": {
+ "typescript": "^5.9.3",
+ "@types/node": "^22.10.7",
+ "@types/react": "^19.0.13",
+ "@types/react-dom": "^19.0.2",
+ "vitest": "^4.0.18"
+ }
+}
\ No newline at end of file
diff --git a/skills/woocommerce-webhooks/examples/nextjs/test/webhook.test.ts b/skills/woocommerce-webhooks/examples/nextjs/test/webhook.test.ts
new file mode 100644
index 0000000..4b4f3d0
--- /dev/null
+++ b/skills/woocommerce-webhooks/examples/nextjs/test/webhook.test.ts
@@ -0,0 +1,214 @@
+import { describe, test, expect, beforeAll } from 'vitest';
+import crypto from 'crypto';
+
+// Test webhook secret
+const TEST_SECRET = 'test_woocommerce_secret_key';
+
+// Set test environment variable
+beforeAll(() => {
+ process.env.WOOCOMMERCE_WEBHOOK_SECRET = TEST_SECRET;
+});
+
+/**
+ * Generate a valid WooCommerce webhook signature for testing
+ * @param payload - JSON payload as string
+ * @param secret - Webhook secret
+ * @returns Base64 encoded signature
+ */
+function generateTestSignature(payload: string, secret: string): string {
+ return crypto
+ .createHmac('sha256', secret)
+ .update(payload)
+ .digest('base64');
+}
+
+// Import the handler after setting environment variables
+const { POST, verifyWooCommerceWebhook } = await import('../app/webhooks/woocommerce/route');
+
+describe('WooCommerce Webhook Handler', () => {
+ describe('Signature Verification', () => {
+ test('should verify valid signatures', () => {
+ const payload = '{"id": 123, "status": "processing"}';
+ const signature = generateTestSignature(payload, TEST_SECRET);
+
+ const isValid = verifyWooCommerceWebhook(payload, signature, TEST_SECRET);
+
+ expect(isValid).toBe(true);
+ });
+
+ test('should reject invalid signatures', () => {
+ const payload = '{"id": 123, "status": "processing"}';
+ const invalidSignature = 'invalid_signature';
+
+ const isValid = verifyWooCommerceWebhook(payload, invalidSignature, TEST_SECRET);
+
+ expect(isValid).toBe(false);
+ });
+
+ test('should reject missing signature', () => {
+ const payload = '{"id": 123, "status": "processing"}';
+
+ const isValid = verifyWooCommerceWebhook(payload, null, TEST_SECRET);
+
+ expect(isValid).toBe(false);
+ });
+
+ test('should reject missing secret', () => {
+ const payload = '{"id": 123, "status": "processing"}';
+ const signature = generateTestSignature(payload, TEST_SECRET);
+
+ const isValid = verifyWooCommerceWebhook(payload, signature, undefined);
+
+ expect(isValid).toBe(false);
+ });
+
+ test('should handle different payload lengths', () => {
+ const payloads = [
+ '{}',
+ '{"id":1}',
+ '{"id": 123, "status": "processing", "total": "29.99", "customer": {"name": "John Doe"}}'
+ ];
+
+ payloads.forEach(payload => {
+ const signature = generateTestSignature(payload, TEST_SECRET);
+ const isValid = verifyWooCommerceWebhook(payload, signature, TEST_SECRET);
+ expect(isValid).toBe(true);
+ });
+ });
+ });
+
+ describe('Webhook Endpoint', () => {
+ test('should accept valid order.created webhook', async () => {
+ const payload = {
+ id: 123,
+ status: 'processing',
+ total: '29.99',
+ currency: 'USD',
+ billing: {
+ first_name: 'John',
+ last_name: 'Doe',
+ email: 'john@example.com'
+ }
+ };
+
+ const payloadString = JSON.stringify(payload);
+ const signature = generateTestSignature(payloadString, TEST_SECRET);
+
+ const request = new Request('http://localhost:3000/webhooks/woocommerce', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-WC-Webhook-Topic': 'order.created',
+ 'X-WC-Webhook-Signature': signature,
+ 'X-WC-Webhook-Source': 'https://example.com',
+ },
+ body: payloadString,
+ });
+
+ const response = await POST(request as any);
+
+ expect(response.status).toBe(200);
+ const body = await response.json();
+ expect(body).toEqual({ received: true });
+ });
+
+ test('should accept valid product.updated webhook', async () => {
+ const payload = {
+ id: 456,
+ name: 'Premium T-Shirt',
+ status: 'publish',
+ regular_price: '29.99',
+ stock_status: 'instock'
+ };
+
+ const payloadString = JSON.stringify(payload);
+ const signature = generateTestSignature(payloadString, TEST_SECRET);
+
+ const request = new Request('http://localhost:3000/webhooks/woocommerce', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-WC-Webhook-Topic': 'product.updated',
+ 'X-WC-Webhook-Signature': signature,
+ 'X-WC-Webhook-Source': 'https://example.com',
+ },
+ body: payloadString,
+ });
+
+ const response = await POST(request as any);
+
+ expect(response.status).toBe(200);
+ const body = await response.json();
+ expect(body).toEqual({ received: true });
+ });
+
+ test('should reject webhook with invalid signature', async () => {
+ const payload = {
+ id: 123,
+ status: 'processing'
+ };
+
+ const request = new Request('http://localhost:3000/webhooks/woocommerce', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-WC-Webhook-Topic': 'order.created',
+ 'X-WC-Webhook-Signature': 'invalid_signature',
+ 'X-WC-Webhook-Source': 'https://example.com',
+ },
+ body: JSON.stringify(payload),
+ });
+
+ const response = await POST(request as any);
+
+ expect(response.status).toBe(400);
+ const body = await response.json();
+ expect(body).toEqual({ error: 'Invalid signature' });
+ });
+
+ test('should reject webhook without signature header', async () => {
+ const payload = {
+ id: 123,
+ status: 'processing'
+ };
+
+ const request = new Request('http://localhost:3000/webhooks/woocommerce', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-WC-Webhook-Topic': 'order.created',
+ 'X-WC-Webhook-Source': 'https://example.com',
+ },
+ body: JSON.stringify(payload),
+ });
+
+ const response = await POST(request as any);
+
+ expect(response.status).toBe(400);
+ const body = await response.json();
+ expect(body).toEqual({ error: 'Invalid signature' });
+ });
+
+ test('should handle malformed JSON gracefully', async () => {
+ const invalidPayload = '{"id": 123, "status":}'; // Invalid JSON
+ const signature = generateTestSignature(invalidPayload, TEST_SECRET);
+
+ const request = new Request('http://localhost:3000/webhooks/woocommerce', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-WC-Webhook-Topic': 'order.created',
+ 'X-WC-Webhook-Signature': signature,
+ 'X-WC-Webhook-Source': 'https://example.com',
+ },
+ body: invalidPayload,
+ });
+
+ const response = await POST(request as any);
+
+ expect(response.status).toBe(500);
+ const body = await response.json();
+ expect(body).toEqual({ error: 'Internal server error' });
+ });
+ });
+});
\ No newline at end of file
diff --git a/skills/woocommerce-webhooks/examples/nextjs/vitest.config.ts b/skills/woocommerce-webhooks/examples/nextjs/vitest.config.ts
new file mode 100644
index 0000000..7054f48
--- /dev/null
+++ b/skills/woocommerce-webhooks/examples/nextjs/vitest.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ test: {
+ environment: 'node',
+ },
+});
\ No newline at end of file
diff --git a/skills/woocommerce-webhooks/references/overview.md b/skills/woocommerce-webhooks/references/overview.md
new file mode 100644
index 0000000..a61cac2
--- /dev/null
+++ b/skills/woocommerce-webhooks/references/overview.md
@@ -0,0 +1,125 @@
+# WooCommerce Webhooks Overview
+
+## What Are WooCommerce Webhooks?
+
+WooCommerce webhooks are HTTP callbacks that fire when specific events happen in your WooCommerce store. When an order is created, a product is updated, or a customer registers, WooCommerce can automatically send a POST request to your application with the event details.
+
+This enables real-time integration between your WooCommerce store and external systems like:
+
+- CRM systems (customer data sync)
+- Email marketing platforms (order confirmations, abandoned carts)
+- Inventory management (stock updates)
+- Analytics platforms (sales tracking)
+- Fulfillment services (order processing)
+
+## Common Event Types
+
+| Event | Triggered When | Common Use Cases |
+|-------|----------------|------------------|
+| `order.created` | New order is placed | Send order confirmation emails, create shipping labels, update inventory |
+| `order.updated` | Order status or details change | Track order fulfillment, send status updates to customers |
+| `order.deleted` | Order is permanently deleted | Clean up external records, reverse inventory changes |
+| `order.restored` | Deleted order is restored | Restore external records, reapply inventory changes |
+| `product.created` | New product is added | Sync to external catalogs, trigger marketing campaigns |
+| `product.updated` | Product details change | Update pricing feeds, sync inventory levels |
+| `product.deleted` | Product is permanently deleted | Remove from external catalogs, update recommendations |
+| `product.restored` | Deleted product is restored | Restore to external catalogs |
+| `customer.created` | New customer account registered | Send welcome emails, add to CRM, create loyalty profiles |
+| `customer.updated` | Customer profile changes | Update CRM records, sync preferences |
+| `customer.deleted` | Customer account deleted | Clean up external profiles, handle GDPR deletion |
+| `coupon.created` | New coupon created | Sync to marketing platforms |
+| `coupon.updated` | Coupon terms modified | Update promotional campaigns |
+| `coupon.deleted` | Coupon removed | End promotional campaigns |
+
+## Event Payload Structure
+
+All WooCommerce webhooks share common payload elements:
+
+```json
+{
+ "id": 123,
+ "date_created": "2024-01-15T10:30:00",
+ "date_modified": "2024-01-15T10:30:00",
+ "status": "processing",
+ // ... event-specific fields
+}
+```
+
+### Order Events
+
+Order webhooks include customer details, line items, totals, and shipping information:
+
+```json
+{
+ "id": 456,
+ "status": "processing",
+ "currency": "USD",
+ "total": "29.99",
+ "billing": {
+ "first_name": "John",
+ "last_name": "Doe",
+ "email": "john@example.com"
+ },
+ "line_items": [
+ {
+ "id": 789,
+ "name": "T-Shirt",
+ "quantity": 1,
+ "price": 29.99
+ }
+ ]
+}
+```
+
+### Product Events
+
+Product webhooks include details like name, price, stock status, and categories:
+
+```json
+{
+ "id": 101,
+ "name": "Premium T-Shirt",
+ "status": "publish",
+ "regular_price": "29.99",
+ "stock_status": "instock",
+ "manage_stock": true,
+ "stock_quantity": 50
+}
+```
+
+### Customer Events
+
+Customer webhooks include profile information and preferences:
+
+```json
+{
+ "id": 202,
+ "email": "customer@example.com",
+ "first_name": "Jane",
+ "last_name": "Smith",
+ "username": "jane_smith",
+ "billing": {
+ "first_name": "Jane",
+ "last_name": "Smith",
+ "company": "Example Corp"
+ }
+}
+```
+
+## Webhook Headers
+
+Every WooCommerce webhook includes these important headers:
+
+- **X-WC-Webhook-Topic** - The event type (e.g., "order.created")
+- **X-WC-Webhook-Resource** - The resource type (e.g., "order")
+- **X-WC-Webhook-Event** - The action (e.g., "created")
+- **X-WC-Webhook-Signature** - HMAC SHA256 signature for verification
+- **X-WC-Webhook-Source** - The store URL that sent the webhook
+- **X-WC-Webhook-ID** - The webhook configuration ID
+- **X-WC-Webhook-Delivery-ID** - Unique identifier for this delivery attempt
+
+## Full Event Reference
+
+For the complete list of events and their payloads, see [WooCommerce's webhook documentation](https://woocommerce.com/document/webhooks/).
+
+The WooCommerce REST API documentation also provides detailed payload schemas for each resource type.
\ No newline at end of file
diff --git a/skills/woocommerce-webhooks/references/setup.md b/skills/woocommerce-webhooks/references/setup.md
new file mode 100644
index 0000000..f4ce24b
--- /dev/null
+++ b/skills/woocommerce-webhooks/references/setup.md
@@ -0,0 +1,110 @@
+# Setting Up WooCommerce Webhooks
+
+## Prerequisites
+
+- WordPress site with WooCommerce plugin installed
+- Admin access to WooCommerce settings
+- Your webhook endpoint URL (where you want to receive webhooks)
+- HTTPS endpoint recommended for production (required for sensitive events)
+
+## Get Your Webhook Secret
+
+The webhook secret is used to generate signatures for verifying webhook authenticity. WooCommerce generates this automatically when you create a webhook, but you can also set a custom one.
+
+1. Go to **WooCommerce > Settings > Advanced > Webhooks**
+2. Click **Add webhook**
+3. The **Secret** field will auto-populate with a secure random string
+4. **Copy and save this secret** - you'll need it for signature verification
+5. Alternatively, you can enter your own custom secret
+
+## Register Your Webhook Endpoint
+
+1. In the **Webhook data** form, fill in:
+ - **Name**: Descriptive name (e.g., "Order Processing Webhook")
+ - **Status**: Set to **Active**
+ - **Topic**: Select the event to listen for (see options below)
+ - **Delivery URL**: Your endpoint URL (e.g., `https://yourapp.com/webhooks/woocommerce`)
+ - **Secret**: Use the auto-generated secret or enter your own
+
+2. **Topic Options**:
+ - **Order created** - `order.created`
+ - **Order updated** - `order.updated`
+ - **Order deleted** - `order.deleted`
+ - **Order restored** - `order.restored`
+ - **Product created** - `product.created`
+ - **Product updated** - `product.updated`
+ - **Product deleted** - `product.deleted`
+ - **Product restored** - `product.restored`
+ - **Customer created** - `customer.created`
+ - **Customer updated** - `customer.updated`
+ - **Customer deleted** - `customer.deleted`
+ - **Coupon created** - `coupon.created`
+ - **Coupon updated** - `coupon.updated`
+ - **Coupon deleted** - `coupon.deleted`
+ - **Action** - Custom action hooks (advanced users)
+
+3. Click **Save Webhook**
+
+## Multiple Webhooks
+
+You can create multiple webhooks for different events or endpoints:
+
+- One webhook for order events → order processing system
+- Another webhook for customer events → CRM system
+- Third webhook for product events → inventory management
+
+Each webhook can have its own delivery URL and secret.
+
+## Test Your Webhook
+
+After saving, WooCommerce automatically sends a test ping to your endpoint. Check your webhook logs to confirm it was received.
+
+You can also trigger test events:
+- Create a test order (for order webhooks)
+- Add a test product (for product webhooks)
+- Register a test customer (for customer webhooks)
+
+## Webhook Management
+
+### View Webhook Status
+
+In **WooCommerce > Settings > Advanced > Webhooks**, you can see:
+- **Status**: Active, Paused, or Disabled
+- **Pending deliveries**: Number of failed deliveries waiting for retry
+- **Last delivery**: Timestamp of most recent delivery attempt
+
+### Automatic Disabling
+
+WooCommerce automatically disables webhooks after 5 consecutive delivery failures. A failure is any response that's not:
+- 2xx (success)
+- 301 (moved permanently)
+- 302 (found/temporary redirect)
+
+### View Webhook Logs
+
+Check delivery logs at **WooCommerce > Status > Logs**:
+1. Select **webhook-delivery** from the "All sources" dropdown
+2. Choose a log file to view delivery details and responses
+3. Use this for debugging connection issues or response errors
+
+## Security Considerations
+
+1. **Use HTTPS**: Always use HTTPS endpoints in production
+2. **Verify Signatures**: Always validate the `X-WC-Webhook-Signature` header
+3. **Keep Secrets Secure**: Store webhook secrets in environment variables
+4. **IP Allowlisting**: Consider restricting access to your store's IP range
+5. **Rate Limiting**: Implement rate limiting on your webhook endpoints
+
+## Custom Topics (Advanced)
+
+For advanced users, you can create custom webhook topics using the `woocommerce_webhook_topic_hooks` filter:
+
+```php
+// In your theme's functions.php or plugin
+add_filter('woocommerce_webhook_topic_hooks', function($topic_hooks) {
+ $topic_hooks['cart.updated'] = ['woocommerce_add_to_cart'];
+ return $topic_hooks;
+});
+```
+
+This creates a custom "cart.updated" topic that triggers when items are added to cart.
\ No newline at end of file
diff --git a/skills/woocommerce-webhooks/references/verification.md b/skills/woocommerce-webhooks/references/verification.md
new file mode 100644
index 0000000..fd17f44
--- /dev/null
+++ b/skills/woocommerce-webhooks/references/verification.md
@@ -0,0 +1,260 @@
+# WooCommerce Signature Verification
+
+## How It Works
+
+WooCommerce signs every webhook request using HMAC SHA-256. The signature is included in the `X-WC-Webhook-Signature` header as a base64-encoded string.
+
+The signature is computed as:
+```
+HMAC-SHA256(raw_request_body, webhook_secret) → base64 encoded
+```
+
+## Implementation
+
+### Node.js
+
+```javascript
+const crypto = require('crypto');
+
+function verifyWooCommerceWebhook(rawBody, signature, secret) {
+ if (!signature || !secret) return false;
+
+ const expectedSignature = crypto
+ .createHmac('sha256', secret)
+ .update(rawBody)
+ .digest('base64');
+
+ // Use timing-safe comparison to prevent timing attacks
+ try {
+ return crypto.timingSafeEqual(
+ Buffer.from(signature),
+ Buffer.from(expectedSignature)
+ );
+ } catch (error) {
+ // Different lengths will throw an error
+ return false;
+ }
+}
+
+// Usage example
+const isValid = verifyWooCommerceWebhook(
+ req.body, // Raw body as Buffer
+ req.headers['x-wc-webhook-signature'], // Signature header
+ process.env.WOOCOMMERCE_WEBHOOK_SECRET // Your secret
+);
+```
+
+### Python
+
+```python
+import hmac
+import hashlib
+import base64
+
+def verify_woocommerce_webhook(raw_body: bytes, signature: str, secret: str) -> bool:
+ if not signature or not secret:
+ return False
+
+ # Generate expected signature
+ hash_digest = hmac.new(
+ secret.encode('utf-8'),
+ raw_body,
+ hashlib.sha256
+ ).digest()
+
+ expected_signature = base64.b64encode(hash_digest).decode('utf-8')
+
+ # Use timing-safe comparison
+ return hmac.compare_digest(signature, expected_signature)
+
+# Usage example
+is_valid = verify_woocommerce_webhook(
+ request.body, # Raw body as bytes
+ request.headers.get('x-wc-webhook-signature'), # Signature header
+ os.environ['WOOCOMMERCE_WEBHOOK_SECRET'] # Your secret
+)
+```
+
+### PHP (Reference Implementation)
+
+WooCommerce itself uses this verification method:
+
+```php
+function verify_woocommerce_webhook($raw_body, $signature, $secret) {
+ if (empty($signature) || empty($secret)) {
+ return false;
+ }
+
+ $expected_signature = base64_encode(
+ hash_hmac('sha256', $raw_body, $secret, true)
+ );
+
+ return hash_equals($signature, $expected_signature);
+}
+
+// Usage example
+$is_valid = verify_woocommerce_webhook(
+ file_get_contents('php://input'), // Raw body
+ $_SERVER['HTTP_X_WC_WEBHOOK_SIGNATURE'], // Signature header
+ $webhook_secret // Your secret
+);
+```
+
+## Common Gotchas
+
+### 1. Use Raw Body, Not Parsed JSON
+
+**❌ Wrong:**
+```javascript
+// DON'T parse the body first
+app.use(express.json());
+app.post('/webhook', (req, res) => {
+ const signature = req.headers['x-wc-webhook-signature'];
+ // req.body is now a JavaScript object, not raw bytes!
+ verifyWooCommerceWebhook(JSON.stringify(req.body), signature, secret);
+});
+```
+
+**✅ Correct:**
+```javascript
+// Use raw body for signature verification
+app.use('/webhooks/woocommerce', express.raw({ type: 'application/json' }));
+app.post('/webhooks/woocommerce', (req, res) => {
+ const signature = req.headers['x-wc-webhook-signature'];
+ // req.body is the raw Buffer
+ if (!verifyWooCommerceWebhook(req.body, signature, secret)) {
+ return res.status(400).send('Invalid signature');
+ }
+
+ // Parse JSON after verification
+ const payload = JSON.parse(req.body);
+});
+```
+
+### 2. Header Name Casing
+
+Different frameworks handle header names differently:
+
+```javascript
+// These are equivalent:
+req.headers['x-wc-webhook-signature']
+req.headers['X-WC-Webhook-Signature']
+req.get('X-WC-Webhook-Signature')
+
+// FastAPI/Python
+request.headers.get('x-wc-webhook-signature')
+request.headers.get('X-WC-Webhook-Signature')
+```
+
+Always use lowercase in your code for consistency.
+
+### 3. Buffer vs String Handling
+
+**Node.js:**
+```javascript
+// Express with express.raw() gives you a Buffer
+if (Buffer.isBuffer(req.body)) {
+ // Use directly
+ verifyWooCommerceWebhook(req.body, signature, secret);
+} else {
+ // Convert string to Buffer
+ verifyWooCommerceWebhook(Buffer.from(req.body), signature, secret);
+}
+```
+
+**Python:**
+```python
+# FastAPI gives you bytes by default with request.body()
+raw_body = await request.body() # This is bytes
+verify_woocommerce_webhook(raw_body, signature, secret)
+
+# If you have a string, encode it
+if isinstance(body, str):
+ body = body.encode('utf-8')
+```
+
+### 4. Missing or Empty Signatures
+
+Always check for missing signatures:
+
+```javascript
+function verifyWooCommerceWebhook(rawBody, signature, secret) {
+ // Guard against missing values
+ if (!signature || !secret || !rawBody) {
+ return false;
+ }
+
+ // Continue with verification...
+}
+```
+
+### 5. Timing Attack Protection
+
+Always use timing-safe comparison functions:
+
+- **Node.js**: `crypto.timingSafeEqual()`
+- **Python**: `hmac.compare_digest()`
+- **PHP**: `hash_equals()`
+
+Never use `===` or `==` for signature comparison.
+
+## Debugging Verification Failures
+
+### 1. Log the Details
+
+```javascript
+function debugWebhook(rawBody, signature, secret) {
+ console.log('Raw body length:', rawBody.length);
+ console.log('Signature received:', signature);
+ console.log('Secret (first 8 chars):', secret?.substring(0, 8));
+
+ const expectedSignature = crypto
+ .createHmac('sha256', secret)
+ .update(rawBody)
+ .digest('base64');
+
+ console.log('Expected signature:', expectedSignature);
+ console.log('Signatures match:', signature === expectedSignature);
+}
+```
+
+### 2. Check WooCommerce Logs
+
+In WooCommerce admin:
+1. Go to **WooCommerce > Status > Logs**
+2. Select **webhook-delivery** logs
+3. Look for HTTP response codes from your endpoint
+4. 400 responses indicate signature verification failure
+
+### 3. Test with Known Data
+
+Create a test case with known values:
+
+```javascript
+const testSecret = 'test_secret';
+const testBody = '{"id":123,"status":"processing"}';
+const expectedSignature = crypto
+ .createHmac('sha256', testSecret)
+ .update(testBody)
+ .digest('base64');
+
+console.log('Test signature:', expectedSignature);
+// Compare with what your verification function produces
+```
+
+### 4. Webhook Delivery Failures
+
+If WooCommerce shows "delivery failures":
+- Check your endpoint is reachable
+- Verify it returns 200 for valid signatures
+- Check for timeouts (WooCommerce has a 60-second limit)
+- Look at your server logs for errors
+
+## Security Best Practices
+
+1. **Always verify signatures** before processing webhook data
+2. **Use HTTPS** in production to prevent man-in-the-middle attacks
+3. **Store secrets securely** in environment variables, not in code
+4. **Implement proper error handling** - return 4xx for bad requests, 5xx for server errors
+5. **Log security events** - track failed verification attempts
+6. **Rate limit** your webhook endpoints to prevent abuse
\ No newline at end of file