How to Sync Variations into Simple Products Between Stores
Recently, a client contacted me about my Simple WP Crossposting plugin, and he asked me whether it is possible to not only sync variable products between WooCommerce stores, but also to automatically convert all variations into simple products when syncing to a specific store.
“Of course, it is possible!” – I said.
In this guide, I am going to share with you how I achieved that, and what are the moments you need to keep in mind. I will also show you how to do that for a WordPress Multisite network, i.e., when using my other plugin – Simple Multisite Crossposting.
Comparing Simple Product Data vs Variation Data
Before diving into the actual solutions, let’s do some exploration first. I mean, when you convert a variation into a simple product, you need to take into consideration that variations in WooCommerce do not have as much data as products do. Besides, the same data of the WC_Product_Variation object may serve a different purpose than in WC_Product_Simple.
To visualize all the data entirely, I created a simple table for you.
By the way, I used WooCommerce REST API product properties as values in the first column.
| Product data | Simple products | Variations |
|---|---|---|
name | Yes | Yes |
slug | Yes | No |
description | Yes | Yes |
short_description | Yes | No |
status | Yes | Partly |
type | Yes | No |
sold_individually | Yes | Parent |
purchase_note | Yes | No |
menu_order | Yes | Partly |
reviews_allowed | Yes | Parent |
catalog_visibility | Yes | Parent |
featured | Yes | Parent |
date_created_gmt | Yes | Parent |
date_modified_gmt | Yes | Parent |
regular_price | Yes | Yes |
sale_price | Yes | Yes |
date_on_sale_from | Yes | Yes |
date_on_sale_to | Yes | Yes |
sku | Yes | Yes |
global_unique_id | Yes | Yes |
manage_stock | Yes | Yes |
stock_quantity | Yes | Yes |
backorders | Yes | Yes |
low_stock_amount | Yes | Yes |
stock_status | Yes | Yes |
is_virtual | Yes | Yes |
weight | Yes | Yes |
dimensions | Yes | Yes |
shipping_class | Yes | Yes |
downloadable | Yes | Yes |
download_limit | Yes | Yes |
download_expiry | Yes | Yes |
downloads | Yes | Yes |
images | Yes | No |
image | No | Yes |
attributes | Yes | Partly |
upsell_ids | Yes | No |
cross_sell_ids | Yes | No |
meta_data | Yes | Yes (but rarely used) |
Now, let’s take a look at those table columns that I marked as “Partly”. What does it mean? I will describe it to you for each property separately:
status– Here, for simple products, we have all the normal post statuses available, likepublish,pending,trash,private, etc. But for variations, we have only two statuses, which represent the behavior of the “Enabled” checkbox – if the checkbox is checked, the status ispublish, if unchecked –private.menu_order– The idea here is that though variations also have this property in their objects, the thing is that they represent a variation position within the product related to other variations. It means that we can not transfer this parameter to converted simple products after all.attributes– For variations, attributes are stored in a more simplified way and have a unique value for each attribute.
That’s pretty much it. Now we’re ready to start syncing products between our stores and convert variations into products.
Convert Variations into Simple Products When Syncing Between Standalone WooCommerce Stores
Let’s start with standalone stores connected with the WooCommerce REST API. If you’re interested in a solution for WordPress multisite networks, you can find it below as well.
Programmatic approach
If you were looking for a solution on how to implement it programmatically, then here it is. Below is just raw code; of course, you need to use it as a part of a custom function, or probably connect to a save_post_product hook or something like that.
Needless to say, the code needs to be used on the initial store (not on the target one).
// it is a variable product ID
$parent_product_id = 12345;
$parent_product = wc_get_product( $parent_product_id );
// it is time to get its variations
$variation_ids = $parent_product->get_children();
if( $variation_ids ) {
// when sending a WooCommerce REST API request we will use batches
$data = array(
'create' => array(),
// sometimes we need to update existing products, in that case, you will need my Simple WP Crossposting plugin
// 'update' => array(),
);
foreach( $variation_ids as $variation_id ) {
$variation = wc_get_product( $variation_id );
// collect data for a simple product
$product_data = array(
'name' => $variation->get_name(),
'description' => $variation->get_description(),
'type' => 'simple',
'images' => array(),
'status' => $variation->get_status(),
'meta_data' => array(),
// prices
'regular_price' => $variation->get_regular_price(),
'sale_price' => $variation->get_sale_price(),
'date_on_sale_from' => $variation->get_date_on_sale_from(),
'date_on_sale_to' => $variation->get_date_on_sale_to(),
// stock information
'sku' => get_post_meta( $variation_id, '_sku', true ),
'global_unique_id' => get_post_meta( $variation_id, '_global_unique_id', true ),
'manage_stock' => $variation->get_manage_stock(),
'stock_quantity' => (int) $variation->get_stock_quantity(),
'backorders' => $variation->get_backorders(),
'low_stock_amount' => ( $variation->get_low_stock_amount() ? (int) $variation->get_low_stock_amount() : null ),
'stock_status' => $variation->get_stock_status(),
'weight' => $variation->get_weight(),
'dimensions' => $variation->get_dimensions(),
'shipping_class' => $variation->get_shipping_class(),
// parent product data
'sold_individually' => $parent_product->get_sold_individually(),
'reviews_allowed' => $parent_product->get_reviews_allowed(),
'catalog_visibility' => $parent_product->get_catalog_visibility(),
'featured' => $parent_product->get_featured(),
);
// image
if( $variation->get_image_id() ) {
$image_url = wp_get_attachment_url( $variation->get_image_id() );
$product_data[ 'images' ][] = array(
'src' => $image_url,
);
}
// meta data
foreach( $variation->get_meta_data() as $meta ) {
$product_data[ 'meta_data' ][] = array(
'key' => $meta->key,
'value' => $meta->value,
);
}
$data[ 'create' ][] = $product_data;
}
$woocommerce->post( 'products/batch', $data );
} Some important key points from the code above:
- The first and most important thing to keep in mind is that a variable product can have quite a number of variations; it could even be a hundred variations. In our case, it means that we must use
/products/batchWooCommerce REST API endpoint, but never/products, because imagine a situation when your custom function will try to send 100+ REST API requests at the same time. Believe me, you won’t like it. - To get a variation’s
skuandglobal_unique_id, I am also using theget_post_meta()function, because if a variation doesn’t have one, when you try to get it from the object, it will try to collect it from the parent product, which we don’t need here. - More about the
$woocommercevariable and connecting to the WooCommerce REST API, you can read in my other article.
The code is heavily simplified, so you might also want the following functionality:
- Copying images added with ACF, for example, in variations custom fields.
- When you sync variations into simple products from a store that is located on a localhost or on another inaccessible server, you might notice that images are not getting through, because, by default, WooCommerce REST API doesn’t support that, and you will need a different approach here.
- Updating existing simple products that have already been synced before. As you can see, the array of data only has the
$data[ 'create' ]subarray, but it doesn’t have the one that allows you to update existing products –$data[ 'update' ]. We need to additionally send a REST API request to check a product ID by the SKU that has already been synced, or simply store the information in the product meta. - Downloadable variations and downloadable products are also not implemented in my code.
If you’re interested in the functionality that I skipped in the code above, please continue reading to the plugin approach, where it is fully implemented.
Using a plugin – Simple WP Crossposting
I’ve been thinking about adding a checkbox to my Simple WP Crossposting plugin’s settings page, like in the screenshot below:

So, basically, when syncing a variable product to a target store, each product’s variation will be synced as a separate simple product and connected by SKU (in other words, if you have variations with “SKU-1”, “SKU-2” on “Store 1”, after crossposting, you will have simple products with the same SKUs on “Store 2”).
But I don’t want to overload the plugin with functionality unless I am 100% sure it is useful and keeps the plugin lightweight as it currently is. So, at the moment, I am still exploring the feedback, whether this feature is really needed or it is just a once-in-a-year request. Please let me know your thoughts in the comments.
For now, this feature is implemented as a separate add-on, which is available after a plugin purchase.

Convert Variations into Simple Products When Syncing Between Subsites Within WordPress Multisite
Ok, but what if your WooCommerce stores are a part of a multisite network? Is there any difference in how we can handle it?
To be honest, everything I described above will work great for multisite networks; however, it will not make any sense to go the WooCommerce REST API route, because it is not only resource-heavy, but also why do you need to establish a connection between sites via the REST API, when your sites are on the same server and share the same database?
That’s why I have different plugins:
- Simple Multisite Crossposting – for multisite networks,
- Simple WP Crossposting – for standalone stores.
Well, that was a little bit of theory; now let’s jump into the programmatic and non-programmatic approaches.
Programmatic approach
There is actually a whole tutorial about creating products programmatically; you can start with it before diving into the code below.
The code is better to use as a part of your custom function or connect to some hook in WooCommerce, like save_post_product or woocommerce_update_product.
// the hardcoded parameters, some of them we can get from the hook arguments as well
$target_blog_id = 2;
$parent_product_id = 12345;
// we still get a WC_Product object and obtain its children (variations)
$parent_product = wc_get_product( $parent_product_id );
// please note, that it is better to double-check that the product is variable
$variation_ids = $parent_product->get_children();
if( $variation_ids ){
switch_to_blog( $target_blog_id );
foreach( $variation_ids as $variation_id ) {
$variation = wc_get_product( $variation_id );
if( ! $variation ){
continue;
}
// we are about to create a simple product
$product = new WC_Product_Simple();
$product->set_name( $variation->get_name() );
$product->set_description( $variation->get_description() );
$product->set_status( $variation->get_status() );
// prices
$product->set_regular_price( $variation->get_regular_price() );
$product->set_sale_price( $variation->get_sale_price() );
$product->set_date_on_sale_from( $variation->get_date_on_sale_from() );
$product->set_date_on_sale_to( $variation->get_date_on_sale_to() );
// inventory
$product->set_sku( get_post_meta( $variation_id, '_sku', true ) );
$product->set_global_unique_id( get_post_meta( $variation_id, '_global_unique_id', true ) );
$product->set_manage_stock( $variation->get_manage_stock() );
$product->set_stock_quantity( $variation->get_stock_quantity() );
$product->set_backorders( $variation->get_backorders() );
$product->set_stock_status( $variation->get_stock_status() );
$product->set_low_stock_amount( $variation->get_low_stock_amount() );
$product->set_weight( $variation->get_weight() );
$product->set_length( $variation->get_length() );
$product->set_width( $variation->get_width() );
$product->set_height( $variation->get_height() );
$product->set_shipping_class_id( $variation->get_shipping_class_id() );
// from its parent product
$product->set_sold_individually( $parent_product->get_sold_individually() );
$product->set_reviews_allowed( $parent_product->get_reviews_allowed() );
$product->set_catalog_visibility( $parent_product->get_catalog_visibility() );
$product->set_featured( $parent_product->get_featured() );
foreach( $variation->get_meta_data() as $meta ) {
$product->add_meta_data( $meta->key, $meta->value );
}
$product->save();
}
restore_current_blog();
}The code is simplified for the sake of the tutorial; if you’re going to develop a functionality based on it, don’t forget about images and downloads.
Speaking of images, when you sync products between sub-sites in a multisite network (or, in our case, variations into products), you will need to programmatically copy images to the target store as well, unless you’re using the Multisite Shared Media Library plugin, of course. If you’re interested in how to do it, please feel free to ask me for hints in the comments below, or you can just use a plugin approach.
Using a plugin – Simple Multisite Crossposting
The idea with the plugin is the same as for the standalone stores. At first, I was thinking about adding a checkbox into the plugin settings, kind of like this one:

But since I’m not quite sure how reasonable it would be to include this functionality in the core version of the plugin, it is available as an extra add-on for Simple Multisite Crossposting.
Have any questions? 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