Building Subscription Products with WooCommerce
Subscription-based business models provide predictable revenue and stronger customer relationships. This guide explores how to implement subscription products in WooCommerce, from basic recurring payments to complex subscription management systems. Learn how to handle billing cycles, manage renewals, and create flexible subscription options that grow your recurring revenue.
Whether you're selling digital services, physical subscription boxes, or membership access, this guide provides the technical knowledge to build robust subscription systems. We'll cover payment integration, customer management, and retention strategies that maximize subscription lifetime value.
Table of Contents
- Subscription Architecture
- Creating Subscription Products
- Payment and Billing Management
- Customer Portal and Management
- Retention and Analytics
- Advanced Subscription Features
Subscription Architecture
Core Subscription System
// Subscription management system
class WC_Subscription_System {
/**
* Subscription statuses
*/
const STATUS_PENDING = 'pending';
const STATUS_ACTIVE = 'active';
const STATUS_ON_HOLD = 'on-hold';
const STATUS_CANCELLED = 'cancelled';
const STATUS_EXPIRED = 'expired';
const STATUS_PENDING_CANCEL = 'pending-cancel';
/**
* Initialize subscription system
*/
public function __construct() {
// Register subscription post type
add_action('init', [$this, 'register_subscription_post_type']);
// Subscription lifecycle hooks
add_action('woocommerce_order_status_completed', [$this, 'activate_subscription']);
add_action('woocommerce_scheduled_subscription_payment', [$this, 'process_renewal_payment']);
add_action('woocommerce_subscription_status_updated', [$this, 'handle_status_change'], 10, 3);
// Cron events
add_action('wc_subscriptions_daily', [$this, 'daily_subscription_tasks']);
// Customer actions
add_action('init', [$this, 'handle_customer_actions']);
}
/**
* Register subscription post type
*/
public function register_subscription_post_type() {
register_post_type('shop_subscription', [
'labels' => [
'name' => __('Subscriptions', 'woocommerce'),
'singular_name' => __('Subscription', 'woocommerce'),
],
'public' => false,
'show_ui' => true,
'show_in_menu' => 'woocommerce',
'capability_type' => 'shop_order',
'map_meta_cap' => true,
'supports' => ['title', 'comments', 'custom-fields'],
]);
}
/**
* Create new subscription
*/
public function create_subscription($args) {
$defaults = [
'customer_id' => 0,
'parent_order_id' => 0,
'product_id' => 0,
'variation_id' => 0,
'quantity' => 1,
'period' => 'month',
'interval' => 1,
'length' => 0, // 0 = unlimited
'trial_period' => '',
'trial_length' => 0,
'start_date' => current_time('mysql'),
'next_payment_date' => '',
'end_date' => '',
'status' => self::STATUS_PENDING,
];
$args = wp_parse_args($args, $defaults);
// Create subscription post
$subscription_id = wp_insert_post([
'post_type' => 'shop_subscription',
'post_status' => 'wc-' . $args['status'],
'post_author' => 1,
'post_title' => sprintf(__('Subscription #%s', 'woocommerce'), uniqid()),
]);
if (is_wp_error($subscription_id)) {
return $subscription_id;
}
// Store subscription data
update_post_meta($subscription_id, '_customer_user', $args['customer_id']);
update_post_meta($subscription_id, '_parent_order_id', $args['parent_order_id']);
update_post_meta($subscription_id, '_product_id', $args['product_id']);
update_post_meta($subscription_id, '_variation_id', $args['variation_id']);
update_post_meta($subscription_id, '_quantity', $args['quantity']);
update_post_meta($subscription_id, '_billing_period', $args['period']);
update_post_meta($subscription_id, '_billing_interval', $args['interval']);
update_post_meta($subscription_id, '_subscription_length', $args['length']);
update_post_meta($subscription_id, '_trial_period', $args['trial_period']);
update_post_meta($subscription_id, '_trial_length', $args['trial_length']);
update_post_meta($subscription_id, '_start_date', $args['start_date']);
// Calculate dates
$this->calculate_subscription_dates($subscription_id);
// Copy billing/shipping from parent order
if ($args['parent_order_id']) {
$this->copy_order_address($args['parent_order_id'], $subscription_id);
}
do_action('woocommerce_subscription_created', $subscription_id, $args);
return $subscription_id;
}
/**
* Calculate subscription dates
*/
private function calculate_subscription_dates($subscription_id) {
$start_date = get_post_meta($subscription_id, '_start_date', true);
$trial_period = get_post_meta($subscription_id, '_trial_period', true);
$trial_length = get_post_meta($subscription_id, '_trial_length', true);
$billing_period = get_post_meta($subscription_id, '_billing_period', true);
$billing_interval = get_post_meta($subscription_id, '_billing_interval', true);
$subscription_length = get_post_meta($subscription_id, '_subscription_length', true);
// Calculate trial end date
if ($trial_period && $trial_length) {
$trial_end = $this->calculate_next_date($start_date, $trial_period, $trial_length);
update_post_meta($subscription_id, '_trial_end_date', $trial_end);
$first_payment = $trial_end;
} else {
$first_payment = $start_date;
}
// Calculate next payment date
$next_payment = $this->calculate_next_date($first_payment, $billing_period, $billing_interval);
update_post_meta($subscription_id, '_next_payment_date', $next_payment);
// Calculate end date
if ($subscription_length > 0) {
$end_date = $this->calculate_next_date($start_date, $billing_period, $subscription_length);
update_post_meta($subscription_id, '_end_date', $end_date);
}
}
/**
* Calculate next date based on period and interval
*/
private function calculate_next_date($from_date, $period, $interval) {
$from_timestamp = strtotime($from_date);
switch ($period) {
case 'day':
$next_timestamp = strtotime("+{$interval} days", $from_timestamp);
break;
case 'week':
$next_timestamp = strtotime("+{$interval} weeks", $from_timestamp);
break;
case 'month':
$next_timestamp = strtotime("+{$interval} months", $from_timestamp);
break;
case 'year':
$next_timestamp = strtotime("+{$interval} years", $from_timestamp);
break;
default:
$next_timestamp = $from_timestamp;
}
return date('Y-m-d H:i:s', $next_timestamp);
}
}
// Initialize subscription system
new WC_Subscription_System();
Creating Subscription Products
Subscription Product Type
// Subscription product implementation
class WC_Product_Subscription extends WC_Product {
protected $product_type = 'subscription';
/**
* Get subscription price
*/
public function get_price($context = 'view') {
return $this->get_prop('subscription_price', $context);
}
/**
* Get subscription period
*/
public function get_subscription_period() {
return $this->get_meta('_subscription_period') ?: 'month';
}
/**
* Get subscription interval
*/
public function get_subscription_interval() {
return $this->get_meta('_subscription_interval') ?: 1;
}
/**
* Get subscription length
*/
public function get_subscription_length() {
return $this->get_meta('_subscription_length') ?: 0;
}
/**
* Get trial period
*/
public function get_trial_period() {
return $this->get_meta('_trial_period') ?: '';
}
/**
* Get trial length
*/
public function get_trial_length() {
return $this->get_meta('_trial_length') ?: 0;
}
/**
* Get sign-up fee
*/
public function get_sign_up_fee() {
return $this->get_meta('_subscription_sign_up_fee') ?: 0;
}
/**
* Get formatted price string
*/
public function get_price_html($deprecated = '') {
$price = $this->get_price();
$sign_up_fee = $this->get_sign_up_fee();
$price_string = wc_price($price);
// Add period
$period = $this->get_subscription_period();
$interval = $this->get_subscription_interval();
if ($interval == 1) {
$price_string .= ' / ' . $period;
} else {
$price_string .= sprintf(' every %d %ss', $interval, $period);
}
// Add sign-up fee
if ($sign_up_fee > 0) {
$price_string .= sprintf(' + %s sign-up fee', wc_price($sign_up_fee));
}
// Add trial
if ($this->get_trial_length() > 0) {
$trial_string = $this->get_trial_string();
$price_string .= ' ' . $trial_string;
}
return apply_filters('woocommerce_subscription_price_html', $price_string, $this);
}
/**
* Get trial string
*/
public function get_trial_string() {
$trial_length = $this->get_trial_length();
$trial_period = $this->get_trial_period();
if ($trial_length > 0) {
return sprintf(
'with %d %s free trial',
$trial_length,
$trial_length == 1 ? $trial_period : $trial_period . 's'
);
}
return '';
}
}
// Register subscription product type
add_filter('product_type_selector', function($types) {
$types['subscription'] = __('Subscription product', 'woocommerce');
return $types;
});
add_filter('woocommerce_product_class', function($classname, $product_type) {
if ($product_type === 'subscription') {
return 'WC_Product_Subscription';
}
return $classname;
}, 10, 2);
Subscription Product Admin
// Admin interface for subscription products
class WC_Subscription_Product_Admin {
public function __construct() {
add_action('woocommerce_product_options_general_product_data', [$this, 'subscription_fields']);
add_action('woocommerce_process_product_meta_subscription', [$this, 'save_subscription_fields']);
add_filter('woocommerce_product_data_tabs', [$this, 'add_subscription_tab']);
add_action('woocommerce_product_data_panels', [$this, 'subscription_data_panel']);
}
/**
* Add subscription fields to general tab
*/
public function subscription_fields() {
global $post;
echo '<div class="options_group subscription_pricing show_if_subscription">';
// Subscription price
woocommerce_wp_text_input([
'id' => '_subscription_price',
'label' => __('Subscription price', 'woocommerce') . ' (' . get_woocommerce_currency_symbol() . ')',
'data_type' => 'price',
]);
// Subscription period
woocommerce_wp_select([
'id' => '_subscription_period',
'label' => __('Subscription period', 'woocommerce'),
'options' => [
'day' => __('day', 'woocommerce'),
'week' => __('week', 'woocommerce'),
'month' => __('month', 'woocommerce'),
'year' => __('year', 'woocommerce'),
],
]);
// Subscription interval
woocommerce_wp_text_input([
'id' => '_subscription_interval',
'label' => __('Subscription interval', 'woocommerce'),
'type' => 'number',
'custom_attributes' => [
'min' => '1',
'step' => '1',
],
'value' => get_post_meta($post->ID, '_subscription_interval', true) ?: 1,
]);
// Subscription length
woocommerce_wp_text_input([
'id' => '_subscription_length',
'label' => __('Subscription length', 'woocommerce'),
'type' => 'number',
'custom_attributes' => [
'min' => '0',
'step' => '1',
],
'description' => __('The length of the subscription. Leave blank for unlimited length.', 'woocommerce'),
'desc_tip' => true,
]);
echo '</div>';
echo '<div class="options_group show_if_subscription">';
// Sign-up fee
woocommerce_wp_text_input([
'id' => '_subscription_sign_up_fee',
'label' => __('Sign-up fee', 'woocommerce') . ' (' . get_woocommerce_currency_symbol() . ')',
'data_type' => 'price',
'description' => __('One-time fee charged at the beginning of the subscription.', 'woocommerce'),
'desc_tip' => true,
]);
echo '</div>';
echo '<div class="options_group show_if_subscription">';
// Trial period
woocommerce_wp_select([
'id' => '_trial_period',
'label' => __('Trial period', 'woocommerce'),
'options' => [
'' => __('No trial', 'woocommerce'),
'day' => __('day', 'woocommerce'),
'week' => __('week', 'woocommerce'),
'month' => __('month', 'woocommerce'),
'year' => __('year', 'woocommerce'),
],
]);
// Trial length
woocommerce_wp_text_input([
'id' => '_trial_length',
'label' => __('Trial length', 'woocommerce'),
'type' => 'number',
'custom_attributes' => [
'min' => '0',
'step' => '1',
],
]);
echo '</div>';
}
}
new WC_Subscription_Product_Admin();
Payment and Billing Management
Recurring Payment Processing
// Recurring payment handler
class WC_Subscription_Payment_Handler {
/**
* Process renewal payment
*/
public function process_renewal_payment($subscription_id) {
$subscription = $this->get_subscription($subscription_id);
if (!$subscription || $subscription->get_status() !== 'active') {
return;
}
try {
// Create renewal order
$renewal_order = $this->create_renewal_order($subscription);
// Get payment method
$payment_method = $subscription->get_payment_method();
$payment_gateway = WC()->payment_gateways()->payment_gateways()[$payment_method];
if (!$payment_gateway || !$payment_gateway->supports('subscriptions')) {
throw new Exception('Payment gateway does not support subscriptions');
}
// Process payment
$result = $payment_gateway->process_subscription_payment(
$renewal_order,
$subscription
);
if ($result['result'] === 'success') {
// Payment successful
$renewal_order->payment_complete($result['transaction_id']);
$subscription->update_dates(['next_payment' => $this->calculate_next_payment_date($subscription)]);
// Send success email
do_action('woocommerce_subscription_renewal_payment_complete', $subscription, $renewal_order);
} else {
// Payment failed
$renewal_order->update_status('failed', $result['message']);
$this->handle_failed_payment($subscription, $renewal_order);
}
} catch (Exception $e) {
$this->log_error('Renewal payment failed: ' . $e->getMessage());
$this->handle_failed_payment($subscription, $renewal_order ?? null);
}
}
/**
* Create renewal order
*/
private function create_renewal_order($subscription) {
$order = wc_create_order([
'customer_id' => $subscription->get_customer_id(),
'created_via' => 'subscription',
]);
// Add subscription items
$product = wc_get_product($subscription->get_product_id());
$order->add_product($product, $subscription->get_quantity());
// Copy addresses
$order->set_address($subscription->get_address('billing'), 'billing');
$order->set_address($subscription->get_address('shipping'), 'shipping');
// Set payment method
$order->set_payment_method($subscription->get_payment_method());
// Link to subscription
$order->update_meta_data('_subscription_renewal', $subscription->get_id());
// Calculate totals
$order->calculate_totals();
$order->save();
return $order;
}
/**
* Handle failed payment
*/
private function handle_failed_payment($subscription, $renewal_order = null) {
$retry_count = $subscription->get_failed_payment_count();
$max_retries = get_option('woocommerce_subscriptions_max_retry_count', 3);
if ($retry_count < $max_retries) {
// Schedule retry
$retry_date = $this->calculate_retry_date($retry_count + 1);
$subscription->update_status('on-hold', 'Payment failed - retry scheduled');
$subscription->update_meta_data('_failed_payment_count', $retry_count + 1);
as_schedule_single_action(
strtotime($retry_date),
'woocommerce_scheduled_subscription_payment_retry',
[$subscription->get_id()]
);
} else {
// Max retries reached - suspend subscription
$subscription->update_status('suspended', 'Payment failed - max retries reached');
do_action('woocommerce_subscription_payment_failed', $subscription, $renewal_order);
}
}
}
Payment Method Management
// Customer payment method management
class WC_Subscription_Payment_Methods {
/**
* Update payment method
*/
public function update_payment_method($subscription_id, $payment_method_token) {
$subscription = wcs_get_subscription($subscription_id);
$token = WC_Payment_Tokens::get($payment_method_token);
// Validate token
if (!$token || $token->get_user_id() !== $subscription->get_customer_id()) {
return new WP_Error('invalid_token', 'Invalid payment method');
}
// Update subscription
$subscription->set_payment_method($token->get_gateway_id());
$subscription->update_meta_data('_payment_token', $token->get_id());
$subscription->update_meta_data('_payment_token_type', $token->get_type());
// Store card details for display
if ($token->get_type() === 'card') {
$subscription->update_meta_data('_payment_card_last4', $token->get_last4());
$subscription->update_meta_data('_payment_card_type', $token->get_card_type());
}
$subscription->save();
// Log change
$subscription->add_order_note(
sprintf('Payment method updated to %s ending in %s',
$token->get_card_type(),
$token->get_last4()
)
);
return true;
}
}
Customer Portal and Management
My Account Subscription Management
// Customer subscription interface
class WC_Subscription_My_Account {
public function __construct() {
// Add menu items
add_filter('woocommerce_account_menu_items', [$this, 'add_menu_items']);
add_action('woocommerce_account_subscriptions_endpoint', [$this, 'subscriptions_content']);
// Register endpoints
add_action('init', [$this, 'add_endpoints']);
// Handle actions
add_action('template_redirect', [$this, 'handle_subscription_actions']);
}
/**
* Add subscription menu items
*/
public function add_menu_items($items) {
$items['subscriptions'] = __('Subscriptions', 'woocommerce');
return $items;
}
/**
* Display subscriptions
*/
public function subscriptions_content() {
$customer_id = get_current_user_id();
$subscriptions = $this->get_customer_subscriptions($customer_id);
wc_get_template('myaccount/subscriptions.php', [
'subscriptions' => $subscriptions,
]);
}
/**
* Handle subscription actions
*/
public function handle_subscription_actions() {
if (!is_account_page() || !isset($_GET['subscription_action'])) {
return;
}
$action = sanitize_key($_GET['subscription_action']);
$subscription_id = absint($_GET['subscription_id']);
// Verify nonce
if (!wp_verify_nonce($_GET['_wpnonce'], 'subscription_action')) {
wc_add_notice(__('Invalid request', 'woocommerce'), 'error');
return;
}
// Verify ownership
$subscription = wcs_get_subscription($subscription_id);
if (!$subscription || $subscription->get_customer_id() !== get_current_user_id()) {
wc_add_notice(__('Invalid subscription', 'woocommerce'), 'error');
return;
}
switch ($action) {
case 'pause':
$this->pause_subscription($subscription);
break;
case 'resume':
$this->resume_subscription($subscription);
break;
case 'cancel':
$this->cancel_subscription($subscription);
break;
case 'reactivate':
$this->reactivate_subscription($subscription);
break;
}
wp_safe_redirect(wc_get_account_endpoint_url('subscriptions'));
exit;
}
}
new WC_Subscription_My_Account();
Retention and Analytics
Subscription Analytics
// Subscription analytics and reporting
class WC_Subscription_Analytics {
/**
* Get subscription metrics
*/
public function get_metrics($date_from = null, $date_to = null) {
global $wpdb;
$metrics = [
'mrr' => $this->calculate_mrr(),
'arr' => $this->calculate_arr(),
'churn_rate' => $this->calculate_churn_rate($date_from, $date_to),
'ltv' => $this->calculate_ltv(),
'new_subscriptions' => $this->count_new_subscriptions($date_from, $date_to),
'cancelled_subscriptions' => $this->count_cancelled_subscriptions($date_from, $date_to),
'active_subscriptions' => $this->count_active_subscriptions(),
'trial_conversions' => $this->calculate_trial_conversion_rate($date_from, $date_to),
];
return $metrics;
}
/**
* Calculate Monthly Recurring Revenue
*/
private function calculate_mrr() {
global $wpdb;
$mrr = $wpdb->get_var("
SELECT SUM(
CASE
WHEN pm2.meta_value = 'day' THEN pm1.meta_value * 30
WHEN pm2.meta_value = 'week' THEN pm1.meta_value * 4.33
WHEN pm2.meta_value = 'month' THEN pm1.meta_value
WHEN pm2.meta_value = 'year' THEN pm1.meta_value / 12
ELSE 0
END
) as mrr
FROM {$wpdb->posts} p
LEFT JOIN {$wpdb->postmeta} pm1 ON p.ID = pm1.post_id AND pm1.meta_key = '_subscription_price'
LEFT JOIN {$wpdb->postmeta} pm2 ON p.ID = pm2.post_id AND pm2.meta_key = '_billing_period'
WHERE p.post_type = 'shop_subscription'
AND p.post_status = 'wc-active'
");
return floatval($mrr);
}
/**
* Calculate churn rate
*/
private function calculate_churn_rate($date_from, $date_to) {
$start_active = $this->count_active_subscriptions($date_from);
$cancelled = $this->count_cancelled_subscriptions($date_from, $date_to);
if ($start_active == 0) {
return 0;
}
return ($cancelled / $start_active) * 100;
}
/**
* Retention cohort analysis
*/
public function get_retention_cohorts($months = 12) {
global $wpdb;
$cohorts = [];
for ($i = $months; $i >= 0; $i--) {
$cohort_date = date('Y-m-01', strtotime("-{$i} months"));
$cohort_end = date('Y-m-t', strtotime("-{$i} months"));
// Get subscriptions started in this cohort
$cohort_subscriptions = $wpdb->get_col($wpdb->prepare("
SELECT ID FROM {$wpdb->posts} p
JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
WHERE p.post_type = 'shop_subscription'
AND pm.meta_key = '_start_date'
AND pm.meta_value >= %s
AND pm.meta_value <= %s
", $cohort_date, $cohort_end));
if (empty($cohort_subscriptions)) {
continue;
}
$cohorts[$cohort_date] = [
'size' => count($cohort_subscriptions),
'retention' => [],
];
// Calculate retention for each subsequent month
for ($j = 0; $j <= $i; $j++) {
$check_date = date('Y-m-d', strtotime("+{$j} months", strtotime($cohort_date)));
$active_count = $this->count_active_in_cohort($cohort_subscriptions, $check_date);
$cohorts[$cohort_date]['retention'][$j] = [
'count' => $active_count,
'percentage' => ($active_count / count($cohort_subscriptions)) * 100,
];
}
}
return $cohorts;
}
}
Advanced Subscription Features
Subscription Switching
// Subscription upgrade/downgrade functionality
class WC_Subscription_Switching {
/**
* Switch subscription
*/
public function switch_subscription($subscription_id, $new_product_id, $args = []) {
$subscription = wcs_get_subscription($subscription_id);
$new_product = wc_get_product($new_product_id);
if (!$subscription || !$new_product || !$new_product->is_type('subscription')) {
return new WP_Error('invalid_request', 'Invalid subscription or product');
}
// Calculate prorated amount
$proration = $this->calculate_proration($subscription, $new_product);
// Create switch order
$switch_order = wc_create_order([
'customer_id' => $subscription->get_customer_id(),
'created_via' => 'subscription_switch',
]);
// Add new product
$switch_order->add_product($new_product, 1);
// Apply proration
if ($proration != 0) {
$fee = new WC_Order_Item_Fee();
$fee->set_name($proration > 0 ? 'Proration credit' : 'Proration charge');
$fee->set_total($proration);
$switch_order->add_item($fee);
}
// Process payment if needed
if ($switch_order->get_total() > 0) {
// Charge customer
$payment_result = $this->process_switch_payment($switch_order, $subscription);
if (!$payment_result) {
return new WP_Error('payment_failed', 'Switch payment failed');
}
}
// Update subscription
$subscription->update_meta_data('_product_id', $new_product_id);
$subscription->update_meta_data('_subscription_price', $new_product->get_price());
// Recalculate dates
$this->recalculate_dates($subscription, $args['next_payment_date'] ?? null);
$subscription->save();
// Log switch
$subscription->add_order_note(
sprintf('Subscription switched from %s to %s',
wc_get_product($subscription->get_product_id())->get_name(),
$new_product->get_name()
)
);
do_action('woocommerce_subscription_switched', $subscription, $new_product, $switch_order);
return $switch_order;
}
/**
* Calculate proration amount
*/
private function calculate_proration($subscription, $new_product) {
$current_price = $subscription->get_total();
$new_price = $new_product->get_price();
// Get remaining days in current period
$next_payment = strtotime($subscription->get_next_payment_date());
$today = current_time('timestamp');
$days_remaining = max(0, ($next_payment - $today) / DAY_IN_SECONDS);
// Calculate daily rates
$period_days = $this->get_period_days($subscription->get_billing_period());
$current_daily_rate = $current_price / $period_days;
$new_daily_rate = $new_price / $period_days;
// Calculate proration
$current_credit = $current_daily_rate * $days_remaining;
$new_charge = $new_daily_rate * $days_remaining;
return $current_credit - $new_charge;
}
}
Building subscription products with WooCommerce requires careful planning of billing cycles, payment processing, and customer management. By implementing robust subscription architecture and focusing on retention strategies, you can create successful recurring revenue streams that provide predictable income and stronger customer relationships.