Bulk Copy and Update Products between WooCommerce Stores
Just another tutorial by your requests guys.
In this one we are about to create a bulk action like on the screenshot below which allows to sync products between two (or multiple) WooCommerce stores.

Before we dive into the coding part, I would like to remind you that there is an add-on which is a part of my Simple WP Crossposting plugin that does exactly the same.
1. Add a New Dropdown Option into Bulk Actions
Let’s begin with adding just a bulk action option into the dropdown, once again, you can find a little bit more information about it in my bulk actions tutorial.
add_filter( 'bulk_actions-edit-product', 'rudr_product_bulk_action' );
function rudr_product_bulk_action( $bulk_actions ) {
$bulk_actions[ 'rudr_crosspost_product' ] = 'Publish/Update on Store 2';
return $bulk_actions;
}
I am sure some of you are wondering – what if you have multiple WooCommerce stores? Not only “Store2”? The long story short, you can just duplicate line 5
miltiple times (for every store) or just use my add-on for that.

That was a simple part and now we are ready for an interesting part 🙂
2. Copy Products or Update Product Data from One Site to Another
Before we begin a couple moments to keep in mind:
- In this tutorial we are talking about two completely standalone WooCommerce sites, that’s why we’re going to use REST API in order to establish a connection between stores. If your stores are a part of a single WordPress Multisite network, then my recommendation for you is to take a closer look at this add-on and at this plugin instead.
- In order to use WooCommerce REST API we have to obtain either a Consumer Key and Consumer Secret or an Application Password (Recommended) of Store 2.
- The WooCommerce REST API endpoing we are using in the code below is
/wp-json/wc/v3/products/batch
, and there is also another one for product variations/wp-json/wc/v3/products/<product_id>/variations/batch
, but our code doesn’t work with variable products, if you need a complete solution, once again, here it is.
add_filter( 'handle_bulk_actions-edit-product', 'rudr_copy_products' );
function rudr_copy_products( $redirect, $doaction, $object_ids ) {
// remove our query args if any
$redirect = remove_query_arg( array( 'so_many_products', 'products_done', 'products_err' ), $redirect );
// do nothing if it is not our bulk action
if( 'rudr_crosspost_product' !== $doaction ) {
return $redirect;
}
// in case more than 100 products selected
if( count( $object_ids ) >= 100 ) {
return add_query_arg( 'so_many_products', $object_ids, $redirect );
}
// now our goal to gather all products data into one array
$products_data = array(
'create' => array();
'update' => array();
);
foreach( $object_ids as $product_id ) {
$product = wc_get_product( $product_id );
// you can always double check
if( ! $product ) {
continue;
}
// get all product data in one go
$product_data = $product->get_data();
// we can process the data before sending in into the request
// for example we can remove what we do not need
unset( $product_data[ 'id' ] );
unset( $product_data[ 'date_created' ] );
unset( $product_data[ 'date_modified' ] );
// we can modify some data as well
if( $product_data[ 'image_id' ] ) {
$image_url = wp_get_attachment_image_url( $product_data[ 'image_id' ] );
unset( $product_data[ 'image_id' ] );
$product_data[ 'images' ] = array(
array(
'src' => $image_url,
),
);
}
// (the same should be done for $product_data[ 'gallery_image_ids' ] )
if( $id = some_function_to_check_if_already_published() ) {
$product_data[ 'id' ] = $id;
$products_data[ 'update' ][] = $product_data;
} else {
$products_data[ 'create' ][] = $product_data;
}
}
// all is ready, let's create a request
$request = wp_remote_post(
'https://STORE-2-URL/wp-json/wc/v3/products/batch',
array(
'headers' => array(
'Authorization' => 'Basic ' . base64_encode( "{$username}:{$application_password}" )
),
'body' => $products_data
)
);
if( 'OK' === wp_remote_retrieve_response_message( $request ) ) {
return add_query_arg( 'products_done', $object_ids, $redirect );
} else {
return add_query_arg( 'products_err', $object_ids, $redirect );
}
}
I believe more clarifications are needed here:
- Lines
13-16
. By default WooCommerce REST API doesn’t handle more than 100 products in a single batch. Of course you can create multiple batch requests by no more than 100 products in each them but an easier way is to ask users to not select a lot of products at once. Below we will display an error message for that. - Line
31
.$product->get_data()
is an amazing method ofWC_Product
class which returns all product data in same format we are going to use to copy it to another site. - Line
39-49
. Here you can see a quite simple piece of code that allows to copy product images to Store 2 as well. Here is a separate tutorial about that. - Line
51
. The functionsome_function_to_check_if_already_published()
is your custom function that allows to check whether product has already been published on Store 2 or not. It also returns the product ID on Store 2. I didn’t included this function code into the tutorial cause there are different ways to check it. The fastest one is to use product meta data, the slowest – additional REST API requests (but if you add transient cache they won’t be so slow). - Line
64
. Once again, if you do not know where to get$username
and$application_password
, please check this tutorial. - You might notice that some product information is still missing – not only gallery images and variations, but also categories and tags, attributes. If you need a complete solution, please take a look at my plugin and its free add-on.
3. Create Admin Notices
The last but not the least – it is quite obvious that we have to display a success message when products are successully published or updated on Store 2 or an error message if something is not right.
For example like this:

And the code:
add_action( 'admin_notices', function() {
if( ! empty( $_REQUEST[ 'so_many_products' ] ) ) {
printf(
'<div id="message" class="error notice is-dismissible"><p>You have selected %s products. Please select less than 100.</p></div>',
absint( $_REQUEST[ 'so_many_products' ] )
);
}
if( ! empty( $_REQUEST[ 'products_done' ] ) ) {
printf( '<div id="message" class="updated notice is-dismissible"><p>' .
_n( '%s product has been successfully published/updated.', '%s products have been successfully published/updated.', absint( $_REQUEST[ 'products_done' ] ) )
. '</p></div>', absint( $_REQUEST[ 'products_done' ] ) );
}
if( ! empty( $_REQUEST[ 'products_err' ] ) ) {
echo '<div id="message" class="error notice is-dismissible"><p>Something went wrong.</p></div>';
}
} );

Misha Rudrastyh
Hey guys and welcome to my website. For more than 10 years I've been doing my best to share with you some superb WordPress guides and tips for free.
Need some developer help? Contact me