How to Bulk Copy WooCommerce Products from One Site to Another
A client asked me about a feature to copy WooCommerce products between his stores, so I decided not only to develop it for him but also to share my experience in this tutorial.
The main goal for this specific tutorial is to create a WordPress bulk action (you can see it in the screenshot below), which allows shop managers to select multiple products from the list and copy them between two (or multiple) WooCommerce stores.
Here is how it is going to work:

As usual, I will show you two ways of achieving our goal – using a plugin for WooCommerce and programmatically.
Copy WooCommerce Products to Another Store Using a Plugin
The easiest way is to use my Simple WordPress Crossposting plugin. However, if you want to copy multiple products at the same time, you will also need the bulk add-on for it (it comes separately, but without any extra charges).
If your sites are a part of a single WooCommerce Multisite network, then it is better to use a multisite version of the plugin. If you are not sure what a multisite network is, then never mind.
1. Install the plugin and the add-on
The first step is super-simple; you just need to get the plugin, get the add-on, and activate both of them on the initial (sending) WooCommerce. It is not necessary to install either the plugin or the add-on on the target (receiving) stores.
As simple as that:

2. Connect to another store where you’re going to copy WooCommerce products to
Before you can copy your WooCommerce products to another site, you need to connect this site in the plugin settings.
To do so, you need to go to Settings > Crosspost, and then switch to the “Sites” tab:

For every store you want to connect, you need to provide a username and application password. Why doesn’t the plugin use consumer key and secret? It is easy – because the plugin is intended to work not only for WooCommerce products but also for regular WordPress posts, pages, and custom post types. It uses the same authentication data.
Have you connected the stores? Great! Now, you have finished the minimum required configuration, and you can copy your products to another WooCommerce store.
Bulk Copy Products Programmatically
1. Add a new dropdown option to the bulk actions
Let’s begin by adding a bulk action option to 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_copy_products' ] = 'Copy to 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 duplicate line 5 multiple times (for every store).

That was a simple part, and now we are ready for an interesting part 🙂
2. Copy products with all product data from one site to another
Before we begin, a couple of moments to keep in mind:
- In this tutorial, we are discussing two completely standalone WooCommerce sites, which is why we’re going to use the REST API to establish a connection between stores. If your stores are a part of a single WordPress Multisite network, then you would probably need the
switch_to_blog()function and my tutorial about creating a product programmatically. - To use the WooCommerce REST API, we have to obtain either a consumer key and consumer secret or an application password for “Store 2”.
- The WooCommerce REST API endpoint 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, I would recommend taking a look at the plugin approach. - Below, we are using the WordPress HTTP API to send requests, but you can use the official PHP library if you want.
Here is the ready code:
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_copy_products' !== $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' ] )
$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( "{$consumer_key}:{$consumer_secret}" )
),
'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, the WooCommerce REST API doesn’t handle more than 100 products in a single batch. Of course, you can create multiple batch requests with no more than 100 products in each of them, but an easier way is to ask users not to 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_Productclass which returns all product data in the 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 you to copy product images to “Store 2” as well. Here is a separate tutorial about that. - Line
51. As you can see here, we only copy new products to another store. It means that if you run the bulk action multiple times, the products will be copied multiple times. So, it is better to check whether the product has already been copied or not to a specific target store. You can collect the IDs of product copies and save them in the original product post meta, for example. Or you can choose the plugin approach, which supports product updates out of the box. - 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.
3. Create admin notices
Last but not least, it is quite obvious that we need to display a success message when products are successfully copied to “Store 2” or an error message in case of an error.
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>';
}
} );Do you have questions about the code or the plugin? Feel free to ask in the comments below.
Misha Rudrastyh
Hey guys and welcome to my website. For more than 15 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