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