This is the “webhooks in” half of the course: something happens in WordPress, and an
n8n workflow wakes up and reacts. If you run WooCommerce, you already have a real,
built-in way to fire that event, no custom code required. If you don’t, you’ll write a
small, standard wp_remote_post() hook that does the same job. Either way, the
destination is the same: n8n’s Webhook trigger node, waiting for an HTTP POST.
A free or self-hosted n8n instance you can log into. For the WooCommerce path, a WooCommerce store with admin access. For the non-WooCommerce path, the ability to add a small must-use plugin or theme functions file to your site.
Step 1: Create the n8n Webhook trigger
Start on the n8n side so you have a URL to send WordPress at. Add a Webhook node as
the first node in a new workflow, set its HTTP Method to POST, and give it a
distinct path, like wp-order-events.
Leave the response mode on “Immediately” for now, that returns a fast acknowledgment to
WordPress without waiting on the rest of the workflow to finish, which matters since
wp_remote_post() and WooCommerce’s own webhook delivery both have request timeouts.
Step 2: WooCommerce sites, use the built-in Webhooks feature
If the site runs WooCommerce, don’t write any code for this part, WooCommerce already ships a real outbound webhooks system. Go to WooCommerce > Settings > Advanced > Webhooks and select Add webhook:
Saving an active webhook sends WooCommerce a ping immediately, confirming the URL is
reachable. Every delivery after that carries an X-WC-Webhook-Topic header and an
X-WC-Webhook-Signature header, a base64-encoded HMAC-SHA256 hash of the payload body
using your secret. WooCommerce automatically disables a webhook after five consecutive
non-2xx responses, so a workflow that’s failing silently will eventually stop bothering
you about it, worth knowing when you’re debugging a “why did deliveries stop” question.
Step 3: Non-WooCommerce sites, a custom wp_remote_post() webhook
Core WordPress has no built-in generic webhooks feature, so the standard pattern is
hooking a real WordPress action and firing your own signed POST with
wp_remote_post():
// File: my-plugin-webhooks.php
add_action( 'transition_post_status', function ( $new_status, $old_status, $post ) {
if ( 'publish' !== $new_status || 'publish' === $old_status || 'post' !== $post->post_type ) {
return;
}
$payload = wp_json_encode( array(
'event' => 'post.published',
'post_id' => $post->ID,
'title' => $post->post_title,
'url' => get_permalink( $post ),
'author_id' => (int) $post->post_author,
'timestamp' => time(),
) );
$secret = defined( 'MY_PLUGIN_WEBHOOK_SECRET' ) ? MY_PLUGIN_WEBHOOK_SECRET : '';
$signature = base64_encode( hash_hmac( 'sha256', $payload, $secret, true ) );
wp_remote_post( 'https://your-n8n-instance.example.com/webhook/wp-order-events', array(
'headers' => array(
'Content-Type' => 'application/json',
'X-WP-Signature' => $signature,
),
'body' => $payload,
'timeout' => 8,
) );
}, 10, 3 );
transition_post_status fires on every status change, so the guard clause matters:
without it, this would fire on every save, not just the first publish. Swap the hook
for whatever real event you’re chasing, woocommerce_order_status_changed,
user_register, comment_post, the shape of the wp_remote_post() call stays the
same regardless of which hook triggers it.
Step 4: Verify the signature on the n8n side
Add a Code node right after the Webhook node to confirm the payload wasn’t forged before anything downstream trusts it:
// n8n Code node
const crypto = require('crypto');
const secret = 'the-same-secret-from-your-php-constant';
const body = JSON.stringify($input.item.json.body);
const expected = crypto.createHmac('sha256', secret).update(body).digest('base64');
const received = $input.item.headers['x-wp-signature'];
if (expected !== received) {
throw new Error('Webhook signature mismatch, discarding payload.');
}
return $input.item;
For the WooCommerce path, the same logic applies against X-WC-Webhook-Signature
instead, with WooCommerce’s secret. Either way, a failed check throws, and the workflow
stops there rather than acting on an unverified request.
Test it
Publish a post (for the custom hook) or place a test order (for WooCommerce), then check the n8n workflow’s Executions tab. You should see one execution with the payload you expect, and the Code node passing without error. If nothing arrives, check that the workflow is activated (Production URL only works when active) and that your site can reach the public internet, some local dev environments block outbound requests by default.
Anyone who discovers your n8n Webhook URL can POST whatever payload they want to it if there’s no verification step. Since these workflows often go on to touch a CRM or send email, an unverified endpoint is an open door for someone to trigger real actions in your other systems using fake WordPress events. Always verify the signature (WooCommerce’s built-in one, or your own HMAC) before any node that has side effects.
Recap
n8n’s Webhook trigger node accepts inbound POST requests at a Test URL while you’re
building and a Production URL once the workflow is activated. WooCommerce sites get a
real, built-in webhooks system for free at WooCommerce > Settings > Advanced >
Webhooks, complete with topic selection and HMAC-signed deliveries. Non-WooCommerce
sites get the same result with a wp_remote_post() call on any WordPress action hook,
signed with your own shared secret. Either way, verify the signature before trusting the
payload downstream. The next lesson runs this in the opposite direction: n8n calling
back into WordPress.
Resources & further reading
- WooCommerce Webhooks documentation, woocommerce.com
- n8n Webhook node documentation, docs.n8n.io
- wp_remote_post() function reference, developer.wordpress.org
- transition_post_status action reference, developer.wordpress.org