Categories
Software Development

Working with WooCommerce Webhooks

I recently went deep into the WooCommerce Webhooks code. I’m preserving my thoughts here (if anyone needs more help — feel free to comment — I’m happy to share what I know).

Webhooks?

TL;DR; A separate website can be notified when you make a sale by creating a webhook.

WooCommerce provides a method to send announcements of actions that occur on your site to any URL. These announcements (topics) consist of WooCommerce domain objects (orders, products, subscriptions, etc. also generically named resources) and include their life-cycle events (creation, updates, deletion, etc.). The WooCommerce documentation has a pretty good writeup about this.

WooCommerce Webhooks Handling Overview

sequenceDiagram

participant WordPress
activate WordPress
WordPress ->> WooCommerce: load woocommerce
activate WooCommerce
WooCommerce ->> Webhooks: instantiate webhooks
activate Webhooks
loop Every webhook
Webhooks ->> Webhooks: hook topics
Note right of Webhooks: An topic is defined by a <br> resource and an event.<br>-<br>A topic has a mapping to one or <br>more WordPress hooks. When the <br>hook fires (i.e. do_action),<br> the webhook is enqueued.<br>-<br>See $webhook->enqueue()
end
deactivate Webhooks
deactivate WooCommerce
WordPress ->> WordPress: ...
WordPress ->> Webhooks: Webhook Event (e.g. Order created)
Note right of Webhooks: Events are added to a global<br>array with: 1️⃣ the Webhook object<br>and 2️⃣ the args passed to the hook.<br>-<br>See $webhook->process()
WordPress ->> WordPress: ...
Note right of WordPress: shutdown
loop Enqueued Webhooks
WordPress ->> Webhooks: deliver
Note right of Webhooks: Actually 2 different methods to <br>deliver. Defaults to async (using<br> Action Scheduler), but can<br>run synchronously.<br>-<br>See $webhook->deliver()
end
deactivate WordPress

Using Webhooks For Fun and Profit

Topics aren’t limited to just WooCommerce domain objects. You can use any WordPress hook to send a webhook. Using the resource “action”, add the hook name as the event and you will receive the first argument passed to the hook as the parameter in your webhook. Events must start with wc_ or woocommerce_.

Example Payload

{
  "action":"woocommerce_my_custom_event",
  "arg":{
     "test":"something I passed in"
  }
}

Deliver custom topics

Using the action resource is limiting.

  • The hook name needs to have a special prefix
  • The shape of the data returned will be wrapped in the arg parameter.
  • WooCommerce uses ActionScheduler for storing/managing async events, capping the size of the data at 191 characters. (Async handling can be disabled to avoid this limitation).

What I really want is my own resources/events. For this example let’s pretend we needed to add our own bookmark object, and we want to send a webhook whenever a bookmark is created.

First, add our topic to the list so it can be appropriately enqueued. This mapping is from topic → hook. When the hook is called (e.g. do_action( 'send_bookmark_created_webhook', $bookmark->id ); ) The webhook is added to the queue.

/**
 * Add webhook topics.
 *
 * This is used in the startup portion of webhooks, and is called for each webhook.  As the webhook has the reference
 * to the specific topic, just return the WordPress action to listen for this topic.
 *
 * @param array $topic_hooks
 * @param \WC_Webhook $webhook
 */
function add_topics( $topic_hooks, $webhook ) {
	$topic_hooks['bookmark.created'] = 'send_bookmark_created_webhook'; // When we create a bookmark, we'll fire this action.
	return $topic_hooks;
}

add_filter( 'woocommerce_webhook_topic_hooks', 'add_topics', 10, 2 );

Now we need to create our payload (avoiding the 191 character limit).

/**
 * Build a payload for our custom topics.
 *
 * @param $filtered_payload mixed  Passed from the existing build_payload function.
 * @param $webhook_resource string Resource parsed from the webhook topic.
 * @param $arg mixed               Args passed to the trigger for the topic.
 * @param $webhook_id int          Can use this to rehydrate a copy of the webhook (e.g. new WC_Webhook( $id )).
 *
 * @return mixed
 */
function build_payload( $filtered_payload, $webhook_resource, $arg, $webhook_id ) {
	if ( 'bookmark' === $webhook_resource ) {
		return get_bookmark( $arg );
	}
	return $filtered_payload;
}

add_filter( 'woocommerce_webhook_payload', 'build_payload', 20, 4 );

Finally, whitelist the webhook.

/**
 * Before the webhook is sent this hook determines if it should be delivered.
 *
 * Have to override this because the custom types.  Alternately could continually overwrite
 * `woocommerce_valid_webhook_resources' and 'woocommerce_valid_webhook_events` like is done
 * in the store currently.
 *
 * @param $should_deliver bool Existing value
 * @param $webhook \WC_Webhook
 * @param $arg mixed           Delivery body
 */
function should_deliver( $should_deliver, $webhook, $arg ) {
	if ( 'bookmark.created' === $webhook->get_topic() ) {
		return true;
	}
	return $should_deliver;
}

add_filter( 'woocommerce_webhook_should_deliver', 'should_deliver', 10, 3 );

At the end of the day

If this helped you or if something remains confusing let me know, I’m interested in hearing people’s pain points and wish list items. Personally, I’m looking at adding retry functionality to this infrastructure and will add another post when I get that sorted.