claude-plugin-wordpress-manager 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +2 -2
- package/CHANGELOG.md +30 -0
- package/agents/wp-content-strategist.md +30 -0
- package/agents/wp-monitoring-agent.md +50 -0
- package/docs/GUIDE.md +249 -15
- package/docs/plans/2026-02-28-wcop-strategic-assessment.md +220 -0
- package/hooks/hooks.json +9 -0
- package/package.json +6 -3
- package/servers/wp-rest-bridge/build/tools/index.d.ts +119 -0
- package/servers/wp-rest-bridge/build/tools/index.js +3 -0
- package/servers/wp-rest-bridge/build/tools/wc-webhooks.d.ts +129 -0
- package/servers/wp-rest-bridge/build/tools/wc-webhooks.js +142 -0
- package/servers/wp-rest-bridge/build/types.d.ts +12 -0
- package/skills/wordpress-router/references/decision-tree.md +7 -3
- package/skills/wp-content/SKILL.md +3 -0
- package/skills/wp-content-repurposing/SKILL.md +96 -0
- package/skills/wp-content-repurposing/references/content-atomization.md +117 -0
- package/skills/wp-content-repurposing/references/email-newsletter.md +136 -0
- package/skills/wp-content-repurposing/references/platform-specs.md +80 -0
- package/skills/wp-content-repurposing/references/social-formats.md +169 -0
- package/skills/wp-content-repurposing/scripts/repurposing_inspect.mjs +140 -0
- package/skills/wp-headless/SKILL.md +1 -0
- package/skills/wp-monitoring/SKILL.md +12 -2
- package/skills/wp-monitoring/references/fleet-monitoring.md +160 -0
- package/skills/wp-monitoring/scripts/monitoring_inspect.mjs +54 -1
- package/skills/wp-webhooks/SKILL.md +107 -0
- package/skills/wp-webhooks/references/integration-recipes.md +176 -0
- package/skills/wp-webhooks/references/payload-formats.md +134 -0
- package/skills/wp-webhooks/references/webhook-security.md +147 -0
- package/skills/wp-webhooks/references/woocommerce-webhooks.md +129 -0
- package/skills/wp-webhooks/references/wordpress-core-webhooks.md +162 -0
- package/skills/wp-webhooks/scripts/webhook_inspect.mjs +157 -0
- package/skills/wp-woocommerce/SKILL.md +1 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Integration Recipes
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Common webhook integration patterns for connecting WordPress to external services. Each recipe shows the webhook configuration and expected behavior.
|
|
6
|
+
|
|
7
|
+
## Zapier Integration
|
|
8
|
+
|
|
9
|
+
**Setup:**
|
|
10
|
+
1. Create a Zap with "Webhooks by Zapier" as trigger (Catch Hook)
|
|
11
|
+
2. Copy the Zapier webhook URL
|
|
12
|
+
3. Create webhook in WordPress:
|
|
13
|
+
```
|
|
14
|
+
Tool: wc_create_webhook (for WooCommerce events)
|
|
15
|
+
name: "Zapier - New Orders"
|
|
16
|
+
topic: "order.created"
|
|
17
|
+
delivery_url: "https://hooks.zapier.com/hooks/catch/123456/abcdef/"
|
|
18
|
+
secret: "zapier-secret"
|
|
19
|
+
```
|
|
20
|
+
4. Test by creating a test order
|
|
21
|
+
5. Configure Zapier actions (email, Slack, Google Sheets, etc.)
|
|
22
|
+
|
|
23
|
+
**Common Zaps:**
|
|
24
|
+
| Trigger | Action | Use Case |
|
|
25
|
+
|---------|--------|----------|
|
|
26
|
+
| New order | Send email | Order confirmation to team |
|
|
27
|
+
| New order | Add row to Google Sheet | Order tracking spreadsheet |
|
|
28
|
+
| New product | Post to Slack | Team notification |
|
|
29
|
+
| New customer | Add to Mailchimp | Email list growth |
|
|
30
|
+
|
|
31
|
+
## Make (Integromat) Integration
|
|
32
|
+
|
|
33
|
+
**Setup:**
|
|
34
|
+
1. Create a scenario with "Webhooks" module (Custom webhook)
|
|
35
|
+
2. Copy the Make webhook URL
|
|
36
|
+
3. Create webhook:
|
|
37
|
+
```
|
|
38
|
+
delivery_url: "https://hook.eu1.make.com/abc123def456"
|
|
39
|
+
```
|
|
40
|
+
4. Run the scenario once to register the webhook structure
|
|
41
|
+
5. Map fields and configure subsequent modules
|
|
42
|
+
|
|
43
|
+
**Common Scenarios:**
|
|
44
|
+
- Order placed → Create invoice in QuickBooks
|
|
45
|
+
- Product updated → Sync to external catalog
|
|
46
|
+
- Customer created → Add to CRM (HubSpot, Pipedrive)
|
|
47
|
+
|
|
48
|
+
## n8n Integration
|
|
49
|
+
|
|
50
|
+
**Setup (self-hosted n8n):**
|
|
51
|
+
1. Create a workflow with "Webhook" trigger node
|
|
52
|
+
2. Set to POST method, configure path
|
|
53
|
+
3. Copy the production webhook URL
|
|
54
|
+
4. Create webhook:
|
|
55
|
+
```
|
|
56
|
+
delivery_url: "https://n8n.example.com/webhook/wordpress-events"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Common Workflows:**
|
|
60
|
+
- Content published → SEO check → Notify team
|
|
61
|
+
- Order → Inventory check → Fulfillment API
|
|
62
|
+
- Customer feedback → Sentiment analysis → Support ticket
|
|
63
|
+
|
|
64
|
+
## Slack Notifications
|
|
65
|
+
|
|
66
|
+
**Setup:**
|
|
67
|
+
1. Create a Slack Incoming Webhook in Slack App settings
|
|
68
|
+
2. Use the mu-plugin approach (WordPress core webhooks) with custom formatting:
|
|
69
|
+
|
|
70
|
+
```php
|
|
71
|
+
add_action('transition_post_status', function($new, $old, $post) {
|
|
72
|
+
if ($new === 'publish' && $old !== 'publish') {
|
|
73
|
+
$slack_url = defined('SLACK_WEBHOOK_URL') ? SLACK_WEBHOOK_URL : '';
|
|
74
|
+
if (!$slack_url) return;
|
|
75
|
+
|
|
76
|
+
wp_remote_post($slack_url, [
|
|
77
|
+
'headers' => ['Content-Type' => 'application/json'],
|
|
78
|
+
'body' => wp_json_encode([
|
|
79
|
+
'text' => sprintf(
|
|
80
|
+
"New %s published: *%s*\n<%s|Read it here>",
|
|
81
|
+
$post->post_type,
|
|
82
|
+
$post->post_title,
|
|
83
|
+
get_permalink($post)
|
|
84
|
+
),
|
|
85
|
+
]),
|
|
86
|
+
]);
|
|
87
|
+
}
|
|
88
|
+
}, 10, 3);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Email Service Integration
|
|
92
|
+
|
|
93
|
+
### Mailchimp / SendGrid
|
|
94
|
+
|
|
95
|
+
**Trigger:** New subscriber or customer event
|
|
96
|
+
**Method:** WooCommerce webhook → Zapier/Make → Mailchimp API
|
|
97
|
+
|
|
98
|
+
Or direct with mu-plugin:
|
|
99
|
+
```php
|
|
100
|
+
add_action('user_register', function($user_id) {
|
|
101
|
+
$user = get_userdata($user_id);
|
|
102
|
+
// POST to Mailchimp API to add subscriber
|
|
103
|
+
wp_remote_post('https://usX.api.mailchimp.com/3.0/lists/LIST_ID/members', [
|
|
104
|
+
'headers' => [
|
|
105
|
+
'Authorization' => 'Bearer ' . MAILCHIMP_API_KEY,
|
|
106
|
+
'Content-Type' => 'application/json',
|
|
107
|
+
],
|
|
108
|
+
'body' => wp_json_encode([
|
|
109
|
+
'email_address' => $user->user_email,
|
|
110
|
+
'status' => 'subscribed',
|
|
111
|
+
]),
|
|
112
|
+
]);
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## CDN Purge
|
|
117
|
+
|
|
118
|
+
### Cloudflare
|
|
119
|
+
|
|
120
|
+
**Trigger:** Content updated
|
|
121
|
+
**Method:** mu-plugin that purges Cloudflare cache on publish:
|
|
122
|
+
|
|
123
|
+
```php
|
|
124
|
+
add_action('transition_post_status', function($new, $old, $post) {
|
|
125
|
+
if ($new === 'publish') {
|
|
126
|
+
wp_remote_post(
|
|
127
|
+
'https://api.cloudflare.com/client/v4/zones/' . CF_ZONE_ID . '/purge_cache',
|
|
128
|
+
[
|
|
129
|
+
'headers' => [
|
|
130
|
+
'Authorization' => 'Bearer ' . CF_API_TOKEN,
|
|
131
|
+
'Content-Type' => 'application/json',
|
|
132
|
+
],
|
|
133
|
+
'body' => wp_json_encode([
|
|
134
|
+
'files' => [get_permalink($post)],
|
|
135
|
+
]),
|
|
136
|
+
]
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}, 10, 3);
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Search Index Update
|
|
143
|
+
|
|
144
|
+
### Algolia / Meilisearch
|
|
145
|
+
|
|
146
|
+
**Trigger:** Content published or updated
|
|
147
|
+
**Method:** WooCommerce webhook or mu-plugin → search API
|
|
148
|
+
|
|
149
|
+
```php
|
|
150
|
+
add_action('save_post', function($post_id, $post) {
|
|
151
|
+
if ($post->post_status !== 'publish') return;
|
|
152
|
+
|
|
153
|
+
wp_remote_post(ALGOLIA_API_URL . '/indexes/posts', [
|
|
154
|
+
'headers' => [
|
|
155
|
+
'X-Algolia-API-Key' => ALGOLIA_ADMIN_KEY,
|
|
156
|
+
'X-Algolia-Application-Id' => ALGOLIA_APP_ID,
|
|
157
|
+
'Content-Type' => 'application/json',
|
|
158
|
+
],
|
|
159
|
+
'body' => wp_json_encode([
|
|
160
|
+
'objectID' => $post_id,
|
|
161
|
+
'title' => $post->post_title,
|
|
162
|
+
'content' => wp_strip_all_tags($post->post_content),
|
|
163
|
+
'url' => get_permalink($post),
|
|
164
|
+
]),
|
|
165
|
+
]);
|
|
166
|
+
}, 10, 2);
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Best Practices
|
|
170
|
+
|
|
171
|
+
- Always use HTTPS for delivery URLs
|
|
172
|
+
- Set a webhook secret and verify signatures on the receiving end
|
|
173
|
+
- Keep webhook timeout under 5 seconds to avoid blocking WordPress
|
|
174
|
+
- Use async processing on the receiving end for heavy operations
|
|
175
|
+
- Monitor webhook delivery logs for failures
|
|
176
|
+
- Group related events when possible to reduce webhook volume
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Webhook Payload Formats
|
|
2
|
+
|
|
3
|
+
## Standard WordPress Webhook Payload
|
|
4
|
+
|
|
5
|
+
When using the mu-plugin approach, payloads follow this structure:
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{
|
|
9
|
+
"event": "post.published",
|
|
10
|
+
"timestamp": "2026-02-28T14:30:00+00:00",
|
|
11
|
+
"site_url": "https://example.com",
|
|
12
|
+
"data": {
|
|
13
|
+
"id": 123,
|
|
14
|
+
"title": "Post Title",
|
|
15
|
+
"slug": "post-title",
|
|
16
|
+
"type": "post",
|
|
17
|
+
"url": "https://example.com/post-title/"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Event Types
|
|
23
|
+
|
|
24
|
+
| Event | Data Fields |
|
|
25
|
+
|-------|-------------|
|
|
26
|
+
| `post.published` | id, title, slug, type, url |
|
|
27
|
+
| `post.updated` | id, title, slug, type, url |
|
|
28
|
+
| `term.updated` | id, name, slug, taxonomy |
|
|
29
|
+
| `user.created` | id, username, email, role |
|
|
30
|
+
| `menu.updated` | menu_id |
|
|
31
|
+
|
|
32
|
+
## WooCommerce Webhook Payloads
|
|
33
|
+
|
|
34
|
+
WooCommerce sends the full resource object as the payload.
|
|
35
|
+
|
|
36
|
+
### Order Created Payload (truncated)
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"id": 456,
|
|
41
|
+
"status": "processing",
|
|
42
|
+
"currency": "EUR",
|
|
43
|
+
"total": "29.99",
|
|
44
|
+
"customer_id": 12,
|
|
45
|
+
"billing": {
|
|
46
|
+
"first_name": "Mario",
|
|
47
|
+
"last_name": "Rossi",
|
|
48
|
+
"email": "mario@example.com",
|
|
49
|
+
"phone": "+39 123 456 7890",
|
|
50
|
+
"address_1": "Via Roma 1",
|
|
51
|
+
"city": "Palermo",
|
|
52
|
+
"state": "PA",
|
|
53
|
+
"postcode": "90100",
|
|
54
|
+
"country": "IT"
|
|
55
|
+
},
|
|
56
|
+
"line_items": [
|
|
57
|
+
{
|
|
58
|
+
"id": 1,
|
|
59
|
+
"name": "Cactus Water - Dolce",
|
|
60
|
+
"product_id": 78,
|
|
61
|
+
"quantity": 6,
|
|
62
|
+
"total": "29.99"
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
"date_created": "2026-02-28T14:30:00",
|
|
66
|
+
"payment_method": "stripe",
|
|
67
|
+
"payment_method_title": "Credit Card"
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Product Updated Payload (truncated)
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"id": 78,
|
|
76
|
+
"name": "Cactus Water - Dolce",
|
|
77
|
+
"slug": "cactus-water-dolce",
|
|
78
|
+
"type": "simple",
|
|
79
|
+
"status": "publish",
|
|
80
|
+
"price": "4.99",
|
|
81
|
+
"regular_price": "5.99",
|
|
82
|
+
"sale_price": "4.99",
|
|
83
|
+
"stock_quantity": 150,
|
|
84
|
+
"stock_status": "instock",
|
|
85
|
+
"categories": [
|
|
86
|
+
{ "id": 3, "name": "Beverages", "slug": "beverages" }
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Custom Payload Formatting
|
|
92
|
+
|
|
93
|
+
### Minimal Payload (reduce bandwidth)
|
|
94
|
+
|
|
95
|
+
If the receiving service only needs specific fields, format a custom payload in the mu-plugin:
|
|
96
|
+
|
|
97
|
+
```php
|
|
98
|
+
send_webhook('order.created', [
|
|
99
|
+
'order_id' => $order->get_id(),
|
|
100
|
+
'total' => $order->get_total(),
|
|
101
|
+
'email' => $order->get_billing_email(),
|
|
102
|
+
'items' => count($order->get_items()),
|
|
103
|
+
]);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Enriched Payload (add computed fields)
|
|
107
|
+
|
|
108
|
+
```php
|
|
109
|
+
send_webhook('post.published', [
|
|
110
|
+
'id' => $post->ID,
|
|
111
|
+
'title' => $post->post_title,
|
|
112
|
+
'url' => get_permalink($post),
|
|
113
|
+
'word_count' => str_word_count(wp_strip_all_tags($post->post_content)),
|
|
114
|
+
'categories' => wp_get_post_categories($post->ID, ['fields' => 'names']),
|
|
115
|
+
'author' => get_the_author_meta('display_name', $post->post_author),
|
|
116
|
+
]);
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Content-Type Headers
|
|
120
|
+
|
|
121
|
+
| Format | Content-Type | When to Use |
|
|
122
|
+
|--------|-------------|-------------|
|
|
123
|
+
| JSON | `application/json` | Default, most integrations |
|
|
124
|
+
| Form-encoded | `application/x-www-form-urlencoded` | Legacy systems |
|
|
125
|
+
| XML | `application/xml` | SOAP integrations |
|
|
126
|
+
|
|
127
|
+
WooCommerce always sends `application/json`. The mu-plugin approach defaults to JSON but can be customized.
|
|
128
|
+
|
|
129
|
+
## Payload Size Considerations
|
|
130
|
+
|
|
131
|
+
- **WooCommerce**: Full resource payloads can be large (5-50 KB for orders with many items)
|
|
132
|
+
- **Custom payloads**: Keep under 1 MB for reliability
|
|
133
|
+
- **Truncation strategy**: For large content fields, send an ID + URL instead of the full body
|
|
134
|
+
- **Batch events**: If multiple events fire in quick succession, consider debouncing on the receiving end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Webhook Security
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Webhook security ensures that only legitimate notifications are processed by the receiving endpoint. Without verification, an attacker could send fake payloads to trigger unauthorized actions.
|
|
6
|
+
|
|
7
|
+
## Shared Secret Authentication (HMAC-SHA256)
|
|
8
|
+
|
|
9
|
+
The standard approach: sender and receiver share a secret key. The sender computes an HMAC hash of the payload and includes it in a header. The receiver recomputes the hash and compares.
|
|
10
|
+
|
|
11
|
+
### Sending (WordPress side)
|
|
12
|
+
|
|
13
|
+
```php
|
|
14
|
+
$payload = wp_json_encode($data);
|
|
15
|
+
$secret = WEBHOOK_SECRET;
|
|
16
|
+
$signature = hash_hmac('sha256', $payload, $secret);
|
|
17
|
+
|
|
18
|
+
wp_remote_post($url, [
|
|
19
|
+
'headers' => [
|
|
20
|
+
'Content-Type' => 'application/json',
|
|
21
|
+
'X-WP-Webhook-Signature' => $signature,
|
|
22
|
+
],
|
|
23
|
+
'body' => $payload,
|
|
24
|
+
]);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Receiving (endpoint side)
|
|
28
|
+
|
|
29
|
+
```javascript
|
|
30
|
+
const crypto = require('crypto');
|
|
31
|
+
|
|
32
|
+
function verifyWebhookSignature(rawBody, signature, secret) {
|
|
33
|
+
const expected = crypto
|
|
34
|
+
.createHmac('sha256', secret)
|
|
35
|
+
.update(rawBody, 'utf8')
|
|
36
|
+
.digest('hex');
|
|
37
|
+
return crypto.timingSafeEqual(
|
|
38
|
+
Buffer.from(signature),
|
|
39
|
+
Buffer.from(expected)
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// In your webhook handler:
|
|
44
|
+
app.post('/webhook', (req, res) => {
|
|
45
|
+
const signature = req.headers['x-wp-webhook-signature'];
|
|
46
|
+
if (!verifyWebhookSignature(req.rawBody, signature, SECRET)) {
|
|
47
|
+
return res.status(401).json({ error: 'Invalid signature' });
|
|
48
|
+
}
|
|
49
|
+
// Process the webhook...
|
|
50
|
+
res.status(200).json({ received: true });
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## WooCommerce Signature Verification
|
|
55
|
+
|
|
56
|
+
WooCommerce uses base64-encoded HMAC-SHA256 in the `X-WC-Webhook-Signature` header:
|
|
57
|
+
|
|
58
|
+
```javascript
|
|
59
|
+
function verifyWooCommerceWebhook(rawBody, signature, secret) {
|
|
60
|
+
const expected = crypto
|
|
61
|
+
.createHmac('sha256', secret)
|
|
62
|
+
.update(rawBody, 'utf8')
|
|
63
|
+
.digest('base64');
|
|
64
|
+
return crypto.timingSafeEqual(
|
|
65
|
+
Buffer.from(signature),
|
|
66
|
+
Buffer.from(expected)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Important:** Always use `crypto.timingSafeEqual()` to prevent timing attacks.
|
|
72
|
+
|
|
73
|
+
## HTTPS Enforcement
|
|
74
|
+
|
|
75
|
+
- **Always use HTTPS** for delivery URLs — never HTTP in production
|
|
76
|
+
- WooCommerce webhooks require HTTPS by default (configurable)
|
|
77
|
+
- Self-signed certificates will cause delivery failures
|
|
78
|
+
- Use Let's Encrypt for free, auto-renewing SSL
|
|
79
|
+
|
|
80
|
+
## IP Allowlisting
|
|
81
|
+
|
|
82
|
+
If the receiving endpoint supports it, restrict incoming webhooks to known IPs:
|
|
83
|
+
|
|
84
|
+
| Source | IP Ranges |
|
|
85
|
+
|--------|-----------|
|
|
86
|
+
| Your WordPress host | Check with your hosting provider |
|
|
87
|
+
| Zapier | Published at zapier.com/help/account/data-management/ip-addresses |
|
|
88
|
+
| Make | Published in Make documentation |
|
|
89
|
+
|
|
90
|
+
**Note:** IP allowlisting is a defense-in-depth measure, not a replacement for signature verification.
|
|
91
|
+
|
|
92
|
+
## Rate Limiting
|
|
93
|
+
|
|
94
|
+
### Sender Side (WordPress)
|
|
95
|
+
- Debounce rapid-fire events (e.g., bulk post updates)
|
|
96
|
+
- Use `wp_schedule_single_event()` for non-urgent webhooks
|
|
97
|
+
- Limit to 1 webhook per resource per 5 seconds
|
|
98
|
+
|
|
99
|
+
### Receiver Side
|
|
100
|
+
- Accept webhooks immediately (200 OK) and process asynchronously
|
|
101
|
+
- Queue incoming webhooks for processing
|
|
102
|
+
- Reject if rate exceeds threshold (429 Too Many Requests)
|
|
103
|
+
|
|
104
|
+
## Timeout Configuration
|
|
105
|
+
|
|
106
|
+
- **WordPress default timeout**: 5 seconds (`wp_remote_post` timeout parameter)
|
|
107
|
+
- **WooCommerce default**: 5 seconds
|
|
108
|
+
- **Recommendation**: Keep timeouts at 5 seconds or less
|
|
109
|
+
- If the receiver needs more time, respond 200 immediately and process in background
|
|
110
|
+
|
|
111
|
+
## Retry Behavior
|
|
112
|
+
|
|
113
|
+
### WooCommerce Built-in Retries
|
|
114
|
+
- Retries up to 5 times on failure
|
|
115
|
+
- Exponential backoff between retries
|
|
116
|
+
- After 5 failures: webhook set to `disabled`
|
|
117
|
+
- Re-enable manually via `wc_update_webhook` with `status: "active"`
|
|
118
|
+
|
|
119
|
+
### Custom Retry (mu-plugin)
|
|
120
|
+
```php
|
|
121
|
+
function send_webhook_with_retry($url, $payload, $max_retries = 3) {
|
|
122
|
+
for ($i = 0; $i < $max_retries; $i++) {
|
|
123
|
+
$response = wp_remote_post($url, [
|
|
124
|
+
'timeout' => 5,
|
|
125
|
+
'headers' => ['Content-Type' => 'application/json'],
|
|
126
|
+
'body' => wp_json_encode($payload),
|
|
127
|
+
]);
|
|
128
|
+
if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
sleep(pow(2, $i)); // Exponential backoff: 1s, 2s, 4s
|
|
132
|
+
}
|
|
133
|
+
error_log("Webhook delivery failed after {$max_retries} retries: {$url}");
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Security Checklist
|
|
139
|
+
|
|
140
|
+
- [ ] HTTPS enforced on all delivery URLs
|
|
141
|
+
- [ ] Shared secret configured and verified
|
|
142
|
+
- [ ] Signature verification uses timing-safe comparison
|
|
143
|
+
- [ ] Payload validated (expected structure and types)
|
|
144
|
+
- [ ] Rate limiting in place on receiver
|
|
145
|
+
- [ ] Timeout set to 5 seconds or less
|
|
146
|
+
- [ ] Failed webhooks logged for monitoring
|
|
147
|
+
- [ ] Secrets stored securely (environment variables, not code)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# WooCommerce Webhooks
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
WooCommerce provides a built-in webhook system via the `wc/v3/webhooks` REST API. Webhooks send JSON payloads to a delivery URL when specific store events occur. This is the preferred approach for WooCommerce event notifications — no custom code needed.
|
|
6
|
+
|
|
7
|
+
## MCP Tools
|
|
8
|
+
|
|
9
|
+
The WP REST Bridge provides 4 tools for managing WooCommerce webhooks:
|
|
10
|
+
|
|
11
|
+
| Tool | Method | Endpoint | Purpose |
|
|
12
|
+
|------|--------|----------|---------|
|
|
13
|
+
| `wc_list_webhooks` | GET | `webhooks` | List all webhooks, optionally filter by status |
|
|
14
|
+
| `wc_create_webhook` | POST | `webhooks` | Create a new webhook with topic and delivery URL |
|
|
15
|
+
| `wc_update_webhook` | PUT | `webhooks/{id}` | Update webhook status, URL, or topic |
|
|
16
|
+
| `wc_delete_webhook` | DELETE | `webhooks/{id}` | Delete a webhook (safety hook requires confirmation) |
|
|
17
|
+
|
|
18
|
+
## Available Topics
|
|
19
|
+
|
|
20
|
+
### Order Topics
|
|
21
|
+
| Topic | Trigger |
|
|
22
|
+
|-------|---------|
|
|
23
|
+
| `order.created` | New order placed |
|
|
24
|
+
| `order.updated` | Order details changed |
|
|
25
|
+
| `order.deleted` | Order deleted |
|
|
26
|
+
| `order.restored` | Order restored from trash |
|
|
27
|
+
|
|
28
|
+
### Product Topics
|
|
29
|
+
| Topic | Trigger |
|
|
30
|
+
|-------|---------|
|
|
31
|
+
| `product.created` | New product published |
|
|
32
|
+
| `product.updated` | Product details changed |
|
|
33
|
+
| `product.deleted` | Product deleted |
|
|
34
|
+
| `product.restored` | Product restored from trash |
|
|
35
|
+
|
|
36
|
+
### Customer Topics
|
|
37
|
+
| Topic | Trigger |
|
|
38
|
+
|-------|---------|
|
|
39
|
+
| `customer.created` | New customer registered |
|
|
40
|
+
| `customer.updated` | Customer profile changed |
|
|
41
|
+
| `customer.deleted` | Customer deleted |
|
|
42
|
+
|
|
43
|
+
### Coupon Topics
|
|
44
|
+
| Topic | Trigger |
|
|
45
|
+
|-------|---------|
|
|
46
|
+
| `coupon.created` | New coupon created |
|
|
47
|
+
| `coupon.updated` | Coupon details changed |
|
|
48
|
+
| `coupon.deleted` | Coupon deleted |
|
|
49
|
+
|
|
50
|
+
### Action Topic
|
|
51
|
+
| Topic | Trigger |
|
|
52
|
+
|-------|---------|
|
|
53
|
+
| `action.woocommerce_*` | Any WooCommerce action hook |
|
|
54
|
+
|
|
55
|
+
## Usage Examples
|
|
56
|
+
|
|
57
|
+
### Create a webhook for new orders
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
Tool: wc_create_webhook
|
|
61
|
+
Params:
|
|
62
|
+
name: "New Order Notification"
|
|
63
|
+
topic: "order.created"
|
|
64
|
+
delivery_url: "https://hooks.zapier.com/hooks/catch/123/abc"
|
|
65
|
+
secret: "my-webhook-secret-key"
|
|
66
|
+
status: "active"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### List active webhooks
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
Tool: wc_list_webhooks
|
|
73
|
+
Params:
|
|
74
|
+
status: "active"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Pause a webhook
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
Tool: wc_update_webhook
|
|
81
|
+
Params:
|
|
82
|
+
id: 42
|
|
83
|
+
status: "paused"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Webhook Status
|
|
87
|
+
|
|
88
|
+
| Status | Description |
|
|
89
|
+
|--------|-------------|
|
|
90
|
+
| `active` | Webhook is delivering payloads |
|
|
91
|
+
| `paused` | Webhook exists but is not delivering |
|
|
92
|
+
| `disabled` | Webhook was disabled (usually after delivery failures) |
|
|
93
|
+
|
|
94
|
+
## Delivery Behavior
|
|
95
|
+
|
|
96
|
+
- **Timeout**: 5 seconds (configurable via `woocommerce_webhook_deliver_async`)
|
|
97
|
+
- **Retries**: WooCommerce retries failed deliveries up to 5 times with exponential backoff
|
|
98
|
+
- **Failure threshold**: After 5 consecutive failures, webhook is automatically set to `disabled`
|
|
99
|
+
- **Logs**: Delivery logs available in WooCommerce > Status > Logs
|
|
100
|
+
|
|
101
|
+
## Webhook Headers
|
|
102
|
+
|
|
103
|
+
WooCommerce sends these headers with every delivery:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
X-WC-Webhook-Source: https://your-site.com/
|
|
107
|
+
X-WC-Webhook-Topic: order.created
|
|
108
|
+
X-WC-Webhook-Resource: order
|
|
109
|
+
X-WC-Webhook-Event: created
|
|
110
|
+
X-WC-Webhook-Signature: <HMAC-SHA256 hash>
|
|
111
|
+
X-WC-Webhook-ID: 42
|
|
112
|
+
X-WC-Webhook-Delivery-ID: <uuid>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Signature Verification
|
|
116
|
+
|
|
117
|
+
The receiving endpoint should verify the `X-WC-Webhook-Signature` header:
|
|
118
|
+
|
|
119
|
+
```javascript
|
|
120
|
+
const crypto = require('crypto');
|
|
121
|
+
|
|
122
|
+
function verifyWebhook(payload, signature, secret) {
|
|
123
|
+
const hash = crypto
|
|
124
|
+
.createHmac('sha256', secret)
|
|
125
|
+
.update(payload, 'utf8')
|
|
126
|
+
.digest('base64');
|
|
127
|
+
return hash === signature;
|
|
128
|
+
}
|
|
129
|
+
```
|