Webhooks In

Triggering an n8n Workflow From a WordPress Webhook

⏱ 17 min

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.

What you'll learn in this lesson
Setting up an n8n Webhook trigger node
Test URL versus Production URL, and why your workflow has to be activated.
WooCommerce's built-in Webhooks feature
WooCommerce > Settings > Advanced > Webhooks, real topics, and the delivery signature.
A custom webhook for non-WooCommerce sites
A wp_remote_post() call on a real WordPress hook, with a shared-secret signature of your own.
Verifying the payload actually came from your site
Why an unverified webhook endpoint is a request forgery risk.
Prerequisites

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.

Test vs. production webhook URLs in n8n
1
While building, use the Test URL
Shown in the node, active only while you click "Listen for Test Event" or the workflow is open in the editor.
2
Activate the workflow for the Production URL
The production webhook only starts accepting requests once the workflow is toggled active, published, not just saved.
3
Point WordPress at whichever URL matches your stage
Test URL while you're wiring things up, Production URL once you flip the workflow live.

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:

WooCommerce webhook setup
1
Name
Anything descriptive, like "n8n: high-value orders".
2
Status
Active, so it delivers immediately once saved.
3
Topic
order.created for this example. Also available: order.updated, product.created/updated, customer.created/updated, coupon.created/updated/deleted, and a custom Action topic tied to any WordPress or WooCommerce action hook.
4
Delivery URL
Paste the n8n Webhook node's URL from Step 1.
5
Secret
A random string. WooCommerce uses it to sign every delivery, keep it out of version control.

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.

A webhook endpoint with no signature check is a forgery risk

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

← When Orchestration Belongs Outside WordPress Calling Your MCP Server From an n8n Flow →