WooCommerce ships with four product types: simple, variable, grouped, and external/affiliate. These cover most ecommerce scenarios. But when your store sells something that doesn’t fit, rental periods, configurable services, composite products with dependencies, you need a custom product type.
Building a custom product type means extending WooCommerce’s product class system, registering your type with the admin interface, handling how it behaves in the cart and checkout, and exposing it through the REST API. It’s the deepest level of WooCommerce customization, and getting it right gives you complete control over how your products work.
This guide walks through the entire process with working code examples.
When You Actually Need a Custom Product Type
Before writing code, make sure you genuinely need a custom product type. WooCommerce’s existing types are flexible, simple products with custom fields and conditional logic handle many use cases.
You need a custom product type when:
- The pricing model is fundamentally different, Per-hour billing, tiered pricing based on usage, or prices that depend on configuration choices that can’t be handled by variations.
- Cart behavior must change, The product needs custom data in the cart (rental dates, service configurations) that persists through checkout and appears on orders.
- The admin editing experience needs restructuring, The standard product data tabs don’t make sense for your product. A rental product needs availability calendars, not inventory counts.
- You need different checkout validation, The product requires validation rules that don’t apply to standard products (date range checks, capacity limits, prerequisite products).
If you just need extra fields on a simple product, use woocommerce_product_options_general_product_data and woocommerce_process_product_meta hooks instead. Much simpler, less maintenance.
Architecture: How WooCommerce Product Types Work
Every WooCommerce product is an instance of a class that extends WC_Product. The class hierarchy:
WC_Product (abstract base)
├── WC_Product_Simple
├── WC_Product_Variable
│ └── WC_Product_Variation
├── WC_Product_Grouped
├── WC_Product_External
└── Your_Custom_Product (extends WC_Product)
WooCommerce determines which class to instantiate using the product_type taxonomy term. When you call wc_get_product( $id ), WooCommerce looks up the product’s type term and maps it to a class via the woocommerce_product_class filter.
This means creating a custom product type requires three things:
- A product class that extends
WC_Product - Registration with WooCommerce’s product type selector
- A class-to-type mapping so WooCommerce instantiates your class
Step 1: Create the Product Class
Start with a class that extends WC_Product. This example creates a “Rental” product type:
class WC_Product_Rental extends WC_Product {
public function get_type() {
return 'rental';
}
public function __construct( $product = 0 ) {
parent::__construct( $product );
}
// Custom properties with getters and setters
public function get_rental_period( $context = 'view' ) {
return $this->get_meta( '_rental_period', true, $context );
}
public function set_rental_period( $value ) {
$this->update_meta_data( '_rental_period', sanitize_text_field( $value ) );
}
public function get_daily_rate( $context = 'view' ) {
return $this->get_meta( '_daily_rate', true, $context );
}
public function set_daily_rate( $value ) {
$this->update_meta_data( '_daily_rate', wc_format_decimal( $value ) );
}
public function get_deposit_amount( $context = 'view' ) {
return $this->get_meta( '_deposit_amount', true, $context );
}
public function set_deposit_amount( $value ) {
$this->update_meta_data( '_deposit_amount', wc_format_decimal( $value ) );
}
// Override: rental products are always purchasable if they have a daily rate
public function is_purchasable() {
return $this->exists() && $this->get_daily_rate() > 0;
}
// Override: rental products are virtual by default
public function is_virtual() {
return false; // Set to true for service-type rentals
}
// Override: customize the price display
public function get_price_html( $deprecated = '' ) {
$daily_rate = $this->get_daily_rate();
if ( $daily_rate > 0 ) {
return wc_price( $daily_rate ) . ' / day';
}
return '';
}
}
Key Method Overrides
| Method | Purpose | When to Override |
|---|---|---|
get_type() |
Returns the product type slug | Always, this identifies your type |
is_purchasable() |
Can this product be added to cart? | When purchase conditions differ from standard |
get_price_html() |
Frontend price display | When pricing format differs (per day, per hour, etc.) |
is_virtual() |
Does this product need shipping? | When your type is always virtual or always physical |
is_sold_individually() |
Limit to one per cart? | For products that can’t be duplicated (service bookings) |
add_to_cart_url() |
URL for the add-to-cart button | When you need a custom flow before adding to cart |
add_to_cart_text() |
Button text on archives | When “Add to cart” doesn’t fit (use “Book Now”, “Rent”) |
Step 2: Register the Product Type
Tell WooCommerce about your new type so it appears in the product data selector and maps to the correct class:
// Register the product type in the selector dropdown
add_filter( 'product_type_selector', function( $types ) {
$types['rental'] = 'Rental Product';
return $types;
} );
// Map the type to your class
add_filter( 'woocommerce_product_class', function( $classname, $product_type ) {
if ( 'rental' === $product_type ) {
return 'WC_Product_Rental';
}
return $classname;
}, 10, 2 );
// Ensure the class file is loaded
add_action( 'init', function() {
require_once plugin_dir_path( __FILE__ ) . 'includes/class-wc-product-rental.php';
} );
Step 3: Build the Admin Interface
WooCommerce product editing uses tabs in the product data metabox. You need to control which tabs show for your type and add custom fields.
Show/Hide Standard Tabs
// Control which product data tabs appear for rental products
add_filter( 'woocommerce_product_data_tabs', function( $tabs ) {
// Show these tabs for rental type
$tabs['general']['class'][] = 'show_if_rental';
$tabs['inventory']['class'][] = 'show_if_rental';
// Hide tabs that don't apply
// Shipping tab will be hidden unless we add 'show_if_rental'
return $tabs;
} );
Add Custom Fields to the General Tab
// Add rental-specific fields to the general product data panel
add_action( 'woocommerce_product_options_general_product_data', function() {
global $post;
echo '<div class="options_group show_if_rental">';
woocommerce_wp_text_input( array(
'id' => '_daily_rate',
'label' => 'Daily Rate (' . get_woocommerce_currency_symbol() . ')',
'desc_tip' => true,
'description' => 'The per-day rental price.',
'type' => 'text',
'data_type' => 'price',
) );
woocommerce_wp_text_input( array(
'id' => '_deposit_amount',
'label' => 'Security Deposit (' . get_woocommerce_currency_symbol() . ')',
'desc_tip' => true,
'description' => 'Refundable deposit required at checkout.',
'type' => 'text',
'data_type' => 'price',
) );
woocommerce_wp_select( array(
'id' => '_rental_period',
'label' => 'Minimum Rental Period',
'options' => array(
'1' => '1 day',
'3' => '3 days',
'7' => '1 week',
'14' => '2 weeks',
'30' => '1 month',
),
) );
echo '</div>';
} );
Save the Custom Fields
add_action( 'woocommerce_process_product_meta_rental', function( $post_id ) {
$product = wc_get_product( $post_id );
if ( isset( $_POST['_daily_rate'] ) ) {
$product->set_daily_rate( sanitize_text_field( wp_unslash( $_POST['_daily_rate'] ) ) );
}
if ( isset( $_POST['_deposit_amount'] ) ) {
$product->set_deposit_amount( sanitize_text_field( wp_unslash( $_POST['_deposit_amount'] ) ) );
}
if ( isset( $_POST['_rental_period'] ) ) {
$product->set_rental_period( sanitize_text_field( wp_unslash( $_POST['_rental_period'] ) ) );
}
$product->save();
} );
JavaScript for Admin Tab Visibility
// Enqueue admin JS to toggle tab visibility
add_action( 'admin_footer', function() {
global $post;
if ( ! $post || 'product' !== $post->post_type ) {
return;
}
?>
<script>
jQuery( function( $ ) {
$( 'body' ).on( 'woocommerce-product-type-change', function( event, type ) {
if ( 'rental' === type ) {
$( '.show_if_rental' ).show();
$( '.hide_if_rental' ).hide();
// Hide the regular price field since we use daily_rate
$( '._regular_price_field' ).closest( '.options_group' ).hide();
}
} );
// Trigger on page load if already rental type
$( '#product-type' ).trigger( 'change' );
} );
</script>
Step 4: Handle Cart and Checkout
Custom product types often need extra data in the cart, rental dates, configuration choices, calculated prices. Here's how to add and display that data:
Add Custom Data to Cart
// Add rental dates to cart item data
add_filter( 'woocommerce_add_cart_item_data', function( $cart_item_data, $product_id ) {
$product = wc_get_product( $product_id );
if ( 'rental' !== $product->get_type() ) {
return $cart_item_data;
}
if ( isset( $_POST['rental_start'] ) && isset( $_POST['rental_end'] ) ) {
$start = sanitize_text_field( wp_unslash( $_POST['rental_start'] ) );
$end = sanitize_text_field( wp_unslash( $_POST['rental_end'] ) );
$cart_item_data['rental_start'] = $start;
$cart_item_data['rental_end'] = $end;
// Calculate total based on days
$days = ( strtotime( $end ) - strtotime( $start ) ) / DAY_IN_SECONDS;
$days = max( 1, intval( $days ) );
$cart_item_data['rental_days'] = $days;
$cart_item_data['rental_total'] = $days * floatval( $product->get_daily_rate() );
// Unique key so same product with different dates creates separate cart items
$cart_item_data['unique_key'] = md5( $product_id . $start . $end );
}
return $cart_item_data;
}, 10, 2 );
// Set the cart item price based on rental calculation
add_action( 'woocommerce_before_calculate_totals', function( $cart ) {
if ( is_admin() && ! defined( 'DOING_AJAX' ) ) {
return;
}
foreach ( $cart->get_cart() as $cart_item ) {
if ( isset( $cart_item['rental_total'] ) ) {
$cart_item['data']->set_price( $cart_item['rental_total'] );
}
}
} );
Display Custom Data in Cart and Orders
// Show rental details in cart
add_filter( 'woocommerce_get_item_data', function( $item_data, $cart_item ) {
if ( isset( $cart_item['rental_start'] ) ) {
$item_data[] = array(
'key' => 'Rental Period',
'value' => date_i18n( 'M j, Y', strtotime( $cart_item['rental_start'] ) )
. ' - '
. date_i18n( 'M j, Y', strtotime( $cart_item['rental_end'] ) ),
);
$item_data[] = array(
'key' => 'Duration',
'value' => $cart_item['rental_days'] . ' days',
);
}
return $item_data;
}, 10, 2 );
// Save rental data to order items
add_action( 'woocommerce_checkout_create_order_line_item', function( $item, $cart_item_key, $values ) {
if ( isset( $values['rental_start'] ) ) {
$item->add_meta_data( '_rental_start', $values['rental_start'] );
$item->add_meta_data( '_rental_end', $values['rental_end'] );
$item->add_meta_data( '_rental_days', $values['rental_days'] );
}
}, 10, 3 );
Step 5: Add to Cart Button and Single Product Template
Override the add-to-cart template to include your custom fields (date pickers, configuration options):
// Use a custom add-to-cart template for rental products
add_action( 'woocommerce_rental_add_to_cart', function() {
wc_get_template(
'single-product/add-to-cart/rental.php',
array(),
'',
plugin_dir_path( __FILE__ ) . 'templates/'
);
} );
// Override button text on shop archives
add_filter( 'woocommerce_product_add_to_cart_text', function( $text, $product ) {
if ( 'rental' === $product->get_type() ) {
return 'Select Dates';
}
return $text;
}, 10, 2 );
Create the template at templates/single-product/add-to-cart/rental.php:
<?php
defined( 'ABSPATH' ) || exit;
global $product;
if ( ! $product->is_purchasable() ) {
return;
}
?>
<form class="cart rental-cart" method="post" enctype="multipart/form-data">
<div class="rental-dates">
<p class="form-row">
<label for="rental_start">Start Date</label>
<input type="date" name="rental_start" id="rental_start"
min="<?php echo esc_attr( date( 'Y-m-d' ) ); ?>" required>
</p>
<p class="form-row">
<label for="rental_end">End Date</label>
<input type="date" name="rental_end" id="rental_end" required>
</p>
</div>
<div class="rental-summary">
<p>Daily rate: <?php echo wc_price( $product->get_daily_rate() ); ?></p>
<?php if ( $product->get_deposit_amount() > 0 ) : ?>
<p>Security deposit: <?php echo wc_price( $product->get_deposit_amount() ); ?></p>
<?php endif; ?>
</div>
<button type="submit" name="add-to-cart"
value="<?php echo esc_attr( $product->get_id() ); ?>"
class="single_add_to_cart_button button alt">
Book Rental
</button>
</form>
Step 6: Validation
Validate custom data before allowing the product to be added to cart:
add_filter( 'woocommerce_add_to_cart_validation', function( $passed, $product_id ) {
$product = wc_get_product( $product_id );
if ( 'rental' !== $product->get_type() ) {
return $passed;
}
$start = isset( $_POST['rental_start'] ) ? sanitize_text_field( wp_unslash( $_POST['rental_start'] ) ) : '';
$end = isset( $_POST['rental_end'] ) ? sanitize_text_field( wp_unslash( $_POST['rental_end'] ) ) : '';
if ( empty( $start ) || empty( $end ) ) {
wc_add_notice( 'Please select both start and end dates.', 'error' );
return false;
}
if ( strtotime( $end ) <= strtotime( $start ) ) {
wc_add_notice( 'End date must be after start date.', 'error' );
return false;
}
if ( strtotime( $start ) < strtotime( 'today' ) ) {
wc_add_notice( 'Start date cannot be in the past.', 'error' );
return false;
}
$min_period = intval( $product->get_rental_period() );
$days = ( strtotime( $end ) - strtotime( $start ) ) / DAY_IN_SECONDS;
if ( $days < $min_period ) {
wc_add_notice(
sprintf( 'Minimum rental period is %d days.', $min_period ),
'error'
);
return false;
}
return $passed;
}, 10, 2 );
Step 7: REST API Support
If your store uses the WooCommerce REST API for headless commerce or mobile apps, expose your custom product data:
// Add custom fields to REST API response
add_filter( 'woocommerce_rest_prepare_product_object', function( $response, $product ) {
if ( 'rental' === $product->get_type() ) {
$response->data['daily_rate'] = $product->get_daily_rate();
$response->data['deposit_amount'] = $product->get_deposit_amount();
$response->data['rental_period'] = $product->get_rental_period();
}
return $response;
}, 10, 2 );
// Allow setting custom fields via REST API
add_action( 'woocommerce_rest_insert_product_object', function( $product, $request ) {
if ( 'rental' === $product->get_type() ) {
if ( isset( $request['daily_rate'] ) ) {
$product->set_daily_rate( $request['daily_rate'] );
}
if ( isset( $request['deposit_amount'] ) ) {
$product->set_deposit_amount( $request['deposit_amount'] );
}
if ( isset( $request['rental_period'] ) ) {
$product->set_rental_period( $request['rental_period'] );
}
$product->save();
}
}, 10, 2 );
HPOS Compatibility
WooCommerce's High-Performance Order Storage (HPOS) changes how order data is stored. If your custom product type adds order meta, make sure you declare HPOS compatibility:
add_action( 'before_woocommerce_init', function() {
if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) {
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility(
'custom_order_tables',
__FILE__,
true
);
}
} );
Use $order->get_meta() and $order->update_meta_data() instead of direct get_post_meta() calls. This ensures your code works with both the legacy posts table and the new orders table.
Testing Your Custom Product Type
Before deploying, verify these scenarios:
| Test Case | What to Check |
|---|---|
| Create product in admin | Type appears in selector, custom fields display, data saves correctly |
| Frontend display | Price format correct, add-to-cart button text, custom template loads |
| Add to cart | Custom data passes to cart, validation works, price calculates correctly |
| Cart display | Custom metadata visible, price shows calculated total |
| Checkout | Order completes, custom data saves to order, email includes details |
| REST API | Custom fields appear in GET response, POST/PUT updates fields |
| HPOS | Works with both legacy and HPOS storage |
| WooCommerce updates | Test with latest WC version, check for deprecated methods |
Real-World Examples
Custom product types power stores that couldn't exist with standard WooCommerce:
- Equipment rental stores, Daily/weekly rates, availability calendars, security deposits, damage waivers. The rental type tracks who has what and when it's due back.
- Custom printing services, Upload-based products where price depends on size, material, quantity, and finish. Each combination calculates a unique price at cart time.
- Subscription boxes, While WooCommerce Subscriptions handles recurring billing, a custom type can manage box contents, customization preferences, and swap deadlines.
- Event tickets, Seat selection, tiered pricing (early bird, VIP, general), date-specific inventory, and automatic expiration after the event.
- Configurable services, Consulting or coaching packages where customers choose hours, add-ons, and scheduling during the purchase flow.
Frequently Asked Questions
Can I extend WC_Product_Simple instead of WC_Product?
Yes. If your custom type is essentially a simple product with extra features, extending WC_Product_Simple gives you all simple product behavior for free. You only override what's different. This is simpler than starting from WC_Product and reimplementing everything.
How do I make my custom product type work with WooCommerce Blocks?
WooCommerce Blocks (used in the block-based cart and checkout) need to recognize your product type. Register your type's data in the Store API using the woocommerce_store_api_product_schema and woocommerce_store_api_product_response filters. Without this, your custom data won't appear in block-based cart views.
Do custom product types work with product bundles and composite products?
It depends on how the bundle plugin discovers product types. Most bundle plugins (WooCommerce Product Bundles, Composite Products) work with any type that extends WC_Product. Test the specific combination before deploying.
How do I handle inventory for custom product types?
If your type uses standard inventory (stock quantity), WooCommerce's built-in inventory system works automatically, just include the inventory tab in your admin UI. For date-based inventory (rental availability per day), you need a custom availability table and checking logic in is_in_stock() and your validation filter.
What happens when WooCommerce updates?
WooCommerce's product class API is stable but evolves. Subscribe to the WooCommerce developer blog for deprecation notices. Key risk areas: changes to the checkout flow (especially with block-based checkout), HPOS migration requirements, and REST API version updates. Always test your custom type against WooCommerce beta releases before major updates.
Start Building
The complete code examples in this guide give you a working rental product type. Adapt the pattern for your specific use case, replace rental dates with your custom fields, adjust the pricing calculation, and modify the cart/checkout behavior.
Keep your custom product type in a dedicated plugin, not in your theme's functions.php. Product types are data-layer logic that should persist regardless of theme changes. Structure it as a proper plugin with security best practices, input sanitization, nonce verification, and capability checks on all admin operations.

