
Woocommerce Subscriptions
- 59 installs
- 41 repo stars
- Updated March 13, 2026
- finsilabs/awesome-ecommerce-skills
Adds subscription products to WooCommerce with automatic recurring billing, renewal notifications, and subscriber self-service management.
About
Configures WooCommerce Subscriptions with billing periods, trials, sign-up fees, prorated plan changes, and gateway integrations like Stripe. A developer uses it to sell memberships, boxes, or SaaS on recurring billing.
- Simple and variable subscription products with trials/fees
- Lifecycle hooks for renewals and failed-payment retries
Woocommerce Subscriptions by the numbers
- 59 all-time installs (skills.sh)
- Ranked #3,167 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill woocommerce-subscriptionsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 59 |
|---|---|
| repo stars | ★ 41 |
| Last updated | March 13, 2026 |
| Repository | finsilabs/awesome-ecommerce-skills ↗ |
What it does
Adds subscription products to WooCommerce with automatic recurring billing, renewal notifications, and subscriber self-service management.
Files
WooCommerce Subscriptions
Overview
WooCommerce Subscriptions (the official premium plugin) adds subscription product types — simple subscriptions and variable subscriptions — with configurable billing periods (daily, weekly, monthly, yearly), free trials, sign-up fees, and prorated upgrades/downgrades. It integrates with Stripe, PayPal Reference Transactions, and other gateways that support automated recurring billing. Custom logic hooks into the subscription lifecycle via a rich set of WordPress actions and filters.
When to Use This Skill
- When selling products or services on a recurring billing schedule (SaaS, memberships, box subscriptions)
- When implementing subscription upgrades, downgrades, or plan switching
- When building custom renewal logic or adding business rules around failed payment retry
- When integrating subscription status with access control (e.g., membership site content gates)
- When extending subscription emails or admin reporting with custom data
- When creating subscription add-ons or per-unit quantity scaling
Core Instructions
1. Create a subscription product programmatically
<?php
// Create a simple subscription product via code (e.g., in a migration script)
$product = new WC_Product_Subscription();
$product->set_name('Monthly Pro Plan');
$product->set_status('publish');
$product->set_regular_price('29.99');
// Save first so the product gets an ID
$product->save();
// Subscription-specific meta (product must be saved before setting post meta)
update_post_meta($product->get_id(), '_subscription_price', '29.99');
update_post_meta($product->get_id(), '_subscription_period', 'month');
update_post_meta($product->get_id(), '_subscription_period_interval', '1');
update_post_meta($product->get_id(), '_subscription_length', '0'); // 0 = forever
update_post_meta($product->get_id(), '_subscription_trial_length', '14');
update_post_meta($product->get_id(), '_subscription_trial_period', 'day');
update_post_meta($product->get_id(), '_subscription_sign_up_fee', '0');2. Hook into subscription lifecycle events
WooCommerce Subscriptions fires specific actions at each lifecycle stage:
<?php
// When a new subscription is created (after successful first payment)
add_action('woocommerce_subscription_status_active', function ($subscription) {
$user_id = $subscription->get_user_id();
$plan = $subscription->get_items(); // Array of WC_Order_Item_Product
// Grant access — e.g., set user role or update capabilities
$user = new WP_User($user_id);
$user->add_role('subscriber_member');
// Track in analytics
error_log("Subscription {$subscription->get_id()} activated for user {$user_id}");
});
// When a renewal payment succeeds
add_action('woocommerce_subscription_renewal_payment_complete', function ($subscription, $last_order) {
$user_id = $subscription->get_user_id();
// Extend access, send renewal receipt, update CRM
do_action('my_plugin_renewal_processed', $user_id, $subscription);
}, 10, 2);
// When a renewal payment fails
add_action('woocommerce_subscription_payment_failed', function ($subscription, $last_order) {
$user_id = $subscription->get_user_id();
$retry_count = $subscription->get_failed_payment_count();
// Notify user and optionally pause access
if ($retry_count >= 2) {
$user = new WP_User($user_id);
$user->remove_role('subscriber_member');
// Send dunning email
$subscription->update_status('on-hold');
}
}, 10, 2);
// When subscription is cancelled
add_action('woocommerce_subscription_status_cancelled', function ($subscription) {
$user_id = $subscription->get_user_id();
$user = new WP_User($user_id);
$user->remove_role('subscriber_member');
});3. Query subscriptions programmatically
<?php
// Get all active subscriptions for a user
function get_user_active_subscriptions(int $user_id): array {
return wcs_get_users_subscriptions($user_id, ['active']);
}
// Check if a user has an active subscription to a specific product
function user_has_active_subscription_to_product(int $user_id, int $product_id): bool {
$subscriptions = wcs_get_subscriptions_for_product($product_id, 'any', ['customer_id' => $user_id]);
foreach ($subscriptions as $subscription) {
if ($subscription->has_status('active')) {
return true;
}
}
return false;
}
// Get subscription by ID
function get_subscription_details(int $subscription_id): ?WC_Subscription {
$subscription = wcs_get_subscription($subscription_id);
if (!$subscription) return null;
return $subscription;
}
// Usage
$subscription = get_subscription_details(1234);
if ($subscription) {
echo $subscription->get_status(); // 'active', 'on-hold', 'cancelled', etc.
echo $subscription->get_next_payment_date(); // ISO 8601 date
echo $subscription->get_total(); // Current billing amount
}4. Handle plan upgrades and downgrades
<?php
// Add proration logic for plan switches
add_filter('woocommerce_subscriptions_switch_proration', function ($proration_amount, $subscription, $new_order, $product, $switch_cart_item) {
// Custom proration: charge/credit based on days remaining in billing period
$next_payment = strtotime($subscription->get_next_payment_date());
$last_payment = $subscription->get_date('last_payment');
$billing_period_days = ($next_payment - strtotime($last_payment)) / DAY_IN_SECONDS;
$days_remaining = ($next_payment - time()) / DAY_IN_SECONDS;
$old_daily_rate = (float)$subscription->get_total() / $billing_period_days;
$new_product_price = (float)$switch_cart_item['data']->get_price();
$new_daily_rate = $new_product_price / $billing_period_days;
// Credit remaining days at old rate, charge at new rate
$proration_amount = ($new_daily_rate - $old_daily_rate) * $days_remaining;
return round($proration_amount, 2);
}, 10, 5);
// Trigger a plan switch programmatically
function switch_subscription_plan(int $subscription_id, int $new_product_id): bool {
$subscription = wcs_get_subscription($subscription_id);
if (!$subscription) return false;
// Remove existing item and add new product
foreach ($subscription->get_items() as $item_id => $item) {
$subscription->remove_item($item_id);
}
$item = new WC_Order_Item_Product();
$item->set_product(wc_get_product($new_product_id));
$item->set_quantity(1);
$subscription->add_item($item);
// Recalculate totals
$subscription->calculate_totals();
$subscription->save();
return true;
}5. Retry failed payments and synchronize billing dates
<?php
// Trigger an immediate payment retry (e.g., from an admin action)
function retry_failed_subscription_payment(int $subscription_id): bool {
$subscription = wcs_get_subscription($subscription_id);
if (!$subscription || !$subscription->has_status('on-hold')) {
return false;
}
// Create a renewal order and attempt payment
$renewal_order = wcs_create_renewal_order($subscription);
if (is_wp_error($renewal_order)) {
return false;
}
// Process payment using the subscription's payment method
$payment_gateway = wc_get_payment_gateway_by_order($subscription);
if ($payment_gateway && method_exists($payment_gateway, 'scheduled_subscription_payment')) {
$payment_gateway->scheduled_subscription_payment(
$renewal_order->get_total(),
$renewal_order
);
}
return true;
}
// Synchronize all active subscriptions to renew on the 1st of the month
add_filter('woocommerce_subscriptions_synced_next_payment_date', function ($next_payment_date, $product, $from_timestamp, $trial_end_timestamp) {
if ($product->get_meta('_subscription_period') === 'month') {
$next_month = date('Y-m-01', strtotime('+1 month'));
return strtotime($next_month);
}
return $next_payment_date;
}, 10, 4);Examples
Suspension and reinstatement flow
<?php
// Admin-triggered suspension with reason logging
function suspend_subscription_with_reason(int $subscription_id, string $reason): void {
$subscription = wcs_get_subscription($subscription_id);
if (!$subscription) return;
// Store suspension reason as meta before status change
$subscription->update_meta_data('_suspension_reason', $reason);
$subscription->update_meta_data('_suspension_date', current_time('mysql'));
$subscription->save();
// Change status to on-hold (fires woocommerce_subscription_status_on-hold action)
$subscription->update_status('on-hold', sprintf('Suspended: %s', $reason));
}
// Reinstate and reset payment date
function reinstate_subscription(int $subscription_id): void {
$subscription = wcs_get_subscription($subscription_id);
if (!$subscription || !$subscription->has_status('on-hold')) return;
// Reset next payment date to avoid immediately triggering a renewal
$subscription->set_date('next_payment', strtotime('+1 month'));
// Activate subscription
$subscription->update_status('active', 'Reinstated by admin');
$subscription->save();
}Custom subscription email
<?php
// Add a custom "Renewal Reminder" email 7 days before renewal
add_filter('woocommerce_email_classes', function ($email_classes) {
require_once plugin_dir_path(__FILE__) . 'class-renewal-reminder-email.php';
$email_classes['My_Renewal_Reminder_Email'] = new My_Renewal_Reminder_Email();
return $email_classes;
});
// Schedule the emails via WooCommerce action scheduler
add_action('woocommerce_scheduled_subscription_payment', function ($subscription_id) {
$subscription = wcs_get_subscription($subscription_id);
if (!$subscription) return;
// Schedule reminder 7 days before next payment
$next_payment = strtotime($subscription->get_next_payment_date()) - (7 * DAY_IN_SECONDS);
if ($next_payment > time()) {
as_schedule_single_action(
$next_payment,
'my_plugin_send_renewal_reminder',
[['subscription_id' => $subscription_id]]
);
}
}, 10);Best Practices
- Use Action Scheduler (bundled with WooCommerce) for all async subscription tasks — never rely on WP cron for payment-critical operations
- Always use `wcs_get_subscription()` with null-checks — subscriptions may be deleted or missing in edge cases (manual deletions, import failures)
- Test payment gateway token handling — subscription renewals require a stored payment token; test that the gateway correctly charges the original card on file during renewal
- Handle failed payment dunning explicitly — the default retry schedule is 1, 4, and 7 days after failure; customize via
wcs_retry_rulesfilter to match your business policy - Log all subscription status changes — store status change history in order notes or a custom table for auditing and customer support
- Respect proration on plan switches — don't force customers to pay double for the same period; use built-in proration or implement custom logic
- Test upgrade/downgrade edge cases — especially when a trial is active or when the billing period changes (monthly to annual)
- Use `wcs_user_has_subscription()` for access control — it's more reliable than checking user roles alone
Common Pitfalls
| Problem | Solution |
|---|---|
| Renewal payments not processing | Check that the payment gateway has supports[] = subscription in its capabilities; gateways must explicitly declare subscription support |
| Subscription status stays "pending" after successful payment | The woocommerce_payment_complete action must fire — verify the payment gateway calls $order->payment_complete() on success |
| Proration results in negative charge | Implement minimum floor of 0.00 in the proration filter return value; negative amounts cause gateway errors on most processors |
wcs_get_subscriptions_for_product returns empty | Pass the product's parent ID for variations — WCS stores the parent product ID, not the variation ID, on subscription items |
| Trial ends but renewal isn't charged | Ensure _subscription_trial_length meta is set to a number > 0 AND the payment gateway token is stored correctly from the initial order |
| Cancellation emails sent on admin status changes | Add remove_action for woocommerce_subscription_status_cancelled before programmatic cancellations and re-add after if you need to suppress emails |
Related Skills
- @woocommerce-plugin-development
- @woocommerce-rest-api
- @subscription-billing
- @stripe-integration
- @woocommerce-blocks
{
"context": "Tests whether the agent uses the correct WooCommerce Subscriptions APIs for payment retry, subscription suspension with meta-logging, and reinstatement with safe next-payment date resetting. Covers wcs_create_renewal_order, payment gateway retrieval, meta data storage, and set_date for next payment.",
"type": "weighted_checklist",
"checklist": [
{
"name": "wcs_get_subscription with null-check (retry)",
"max_score": 6,
"description": "The retry function calls wcs_get_subscription() and checks for falsy result before proceeding"
},
{
"name": "On-hold status check before retry",
"max_score": 8,
"description": "The retry function verifies the subscription has 'on-hold' status (e.g., has_status('on-hold')) before attempting payment"
},
{
"name": "wcs_create_renewal_order",
"max_score": 10,
"description": "Uses wcs_create_renewal_order() to create the renewal order for retry (not manually creating a WC_Order)"
},
{
"name": "WP_Error check on renewal order",
"max_score": 8,
"description": "Checks is_wp_error($renewal_order) and returns early if the order creation failed"
},
{
"name": "Gateway retrieval",
"max_score": 8,
"description": "Uses wc_get_payment_gateway_by_order() to retrieve the payment gateway associated with the subscription"
},
{
"name": "scheduled_subscription_payment called",
"max_score": 8,
"description": "Calls $payment_gateway->scheduled_subscription_payment() with the renewal order total and order object"
},
{
"name": "method_exists check on gateway",
"max_score": 6,
"description": "Checks method_exists($payment_gateway, 'scheduled_subscription_payment') before calling it"
},
{
"name": "Suspension meta: reason",
"max_score": 8,
"description": "The suspend function stores the suspension reason as meta using update_meta_data('_suspension_reason', ...) before changing status"
},
{
"name": "Suspension meta: date",
"max_score": 8,
"description": "The suspend function stores the suspension timestamp as meta using update_meta_data('_suspension_date', ...) before changing status"
},
{
"name": "update_status to on-hold",
"max_score": 8,
"description": "The suspend function calls update_status('on-hold', ...) to put the subscription on hold (not a direct status property assignment)"
},
{
"name": "On-hold check before reinstatement",
"max_score": 6,
"description": "The reinstate function verifies the subscription has 'on-hold' status before proceeding"
},
{
"name": "set_date for next_payment",
"max_score": 10,
"description": "The reinstate function calls set_date('next_payment', ...) to push the next billing date forward before activating"
},
{
"name": "update_status to active on reinstatement",
"max_score": 6,
"description": "The reinstate function calls update_status('active', ...) to reactivate the subscription"
}
]
}
Subscription Recovery and Admin Suspension Tools
Problem/Feature Description
A subscription box company is experiencing revenue leakage from failed recurring payments. Their customer success team needs two capabilities: first, an admin tool that can trigger an immediate payment retry for customers whose subscriptions have gone on hold due to a failed renewal — without waiting for the automatic retry schedule. Second, the operations team occasionally needs to suspend a subscription manually (e.g., due to fraud review or shipping address issues) and later reinstate it without accidentally triggering an immediate renewal charge on the customer.
The team also wants an audit trail: when a subscription is suspended manually, the reason and timestamp should be recorded. When it's reinstated, the next billing date should be pushed forward to give the customer a fair billing period, rather than charging them immediately after a suspension.
Output Specification
Write a PHP file at subscription-recovery-tools.php that implements:
1. A function to trigger an immediate payment retry for an on-hold subscription, using the subscription's stored payment method. 2. A function to suspend a subscription with a reason string, logging the reason and date before changing status. 3. A function to reinstate a previously suspended subscription, resetting the next payment date appropriately to avoid an immediate charge.
Include inline comments. The file should be self-contained.
{
"context": "Tests whether the agent uses the correct WooCommerce Subscriptions filter for proration, implements proper daily-rate proration calculation, enforces a non-negative floor, and uses the correct programmatic plan-switch sequence with calculate_totals and save.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Correct proration filter",
"max_score": 10,
"description": "Uses the woocommerce_subscriptions_switch_proration filter (not a custom hook or direct function override)"
},
{
"name": "Filter parameter count",
"max_score": 8,
"description": "The proration filter callback accepts all 5 parameters: $proration_amount, $subscription, $new_order, $product, $switch_cart_item with priority 10"
},
{
"name": "Next payment date used",
"max_score": 8,
"description": "Uses $subscription->get_next_payment_date() to determine the end of the billing period"
},
{
"name": "Last payment date used",
"max_score": 8,
"description": "Uses $subscription->get_date('last_payment') to determine the start of the billing period"
},
{
"name": "Daily rate calculation",
"max_score": 8,
"description": "Calculates both old and new daily rates by dividing plan price by the number of days in the billing period"
},
{
"name": "Days remaining used",
"max_score": 8,
"description": "Multiplies the rate difference by remaining days (not total days) to produce the proration amount"
},
{
"name": "Non-negative floor",
"max_score": 10,
"description": "Ensures the returned proration amount is never less than 0.00 (uses max(), a conditional, or similar guard)"
},
{
"name": "Rounded result",
"max_score": 6,
"description": "Returns the proration amount rounded to 2 decimal places using round()"
},
{
"name": "wcs_get_subscription null-check",
"max_score": 8,
"description": "The plan-switch function calls wcs_get_subscription() and returns early (false/null) if the result is falsy"
},
{
"name": "Remove existing items",
"max_score": 8,
"description": "Iterates over subscription items and calls remove_item() on each before adding the new product"
},
{
"name": "WC_Order_Item_Product used",
"max_score": 8,
"description": "Creates a WC_Order_Item_Product instance and uses set_product() and set_quantity() to configure it"
},
{
"name": "calculate_totals and save",
"max_score": 10,
"description": "Calls $subscription->calculate_totals() followed by $subscription->save() after adding the new item"
}
]
}
Subscription Plan Upgrade/Downgrade with Fair Billing
Problem/Feature Description
A SaaS company offers three subscription tiers — Starter ($9.99/month), Professional ($29.99/month), and Enterprise ($79.99/month) — and customers frequently upgrade or downgrade mid-billing-cycle. The finance team has received complaints that when a customer switches plans, they are either charged twice for the same period or receive no credit for days already paid. The company wants customers to be charged fairly: credited for unused days on their current plan and charged only the proportional cost of the new plan for the remainder of the billing period.
Additionally, customers who switch to a lower tier should never see a negative charge on their invoice — the system should cap any credits so that the switch results in a zero or positive amount only. The engineering team also needs a reliable function to actually perform the plan switch itself (update the subscription items, recalculate totals, and persist the change).
Output Specification
Write a PHP file at subscription-plan-switcher.php that:
1. Registers a proration filter that calculates the prorated amount for plan switches using daily rates and days remaining in the billing period. 2. Ensures the prorated amount is never negative. 3. Provides a function that programmatically switches a subscription from one product to another, properly removing old items and adding the new product.
Include inline comments explaining the proration logic. The file should be self-contained with all necessary add_filter / add_action calls.
{
"context": "Tests whether the agent uses the correct WooCommerce Subscriptions classes, meta keys, and lifecycle action hooks when building a subscription product plugin. Covers programmatic product creation with correct meta, lifecycle hook registration, and failed payment dunning logic.",
"type": "weighted_checklist",
"checklist": [
{
"name": "WC_Product_Subscription class",
"max_score": 8,
"description": "Uses WC_Product_Subscription (not WC_Product or another product class) to instantiate the subscription product"
},
{
"name": "Subscription price meta",
"max_score": 8,
"description": "Sets _subscription_price meta via update_post_meta (not only set_regular_price)"
},
{
"name": "Subscription period meta",
"max_score": 8,
"description": "Sets _subscription_period meta (e.g., 'month') via update_post_meta"
},
{
"name": "Period interval meta",
"max_score": 6,
"description": "Sets _subscription_period_interval meta via update_post_meta"
},
{
"name": "Subscription length meta",
"max_score": 6,
"description": "Sets _subscription_length meta via update_post_meta (0 for forever)"
},
{
"name": "Trial meta keys",
"max_score": 8,
"description": "Sets both _subscription_trial_length AND _subscription_trial_period meta via update_post_meta"
},
{
"name": "Sign-up fee meta",
"max_score": 6,
"description": "Sets _subscription_sign_up_fee meta via update_post_meta"
},
{
"name": "Activation hook",
"max_score": 8,
"description": "Uses woocommerce_subscription_status_active action hook to handle new subscription activation"
},
{
"name": "Renewal success hook",
"max_score": 8,
"description": "Uses woocommerce_subscription_renewal_payment_complete action hook to handle successful renewals"
},
{
"name": "Payment failed hook",
"max_score": 8,
"description": "Uses woocommerce_subscription_payment_failed action hook to handle renewal failures"
},
{
"name": "Cancellation hook",
"max_score": 8,
"description": "Uses woocommerce_subscription_status_cancelled action hook to handle cancellations"
},
{
"name": "Failure count check",
"max_score": 8,
"description": "Calls get_failed_payment_count() on the subscription to determine number of failures before taking additional action"
},
{
"name": "On-hold on repeated failure",
"max_score": 10,
"description": "Calls WC_Subscriptions_Manager::put_subscription_on_hold_for_user() when failure count reaches 2 or more"
}
]
}
Premium Membership Plugin Setup
Problem/Feature Description
A digital media company is launching a new premium content tier called "Premium Membership" on their WooCommerce store. They want members to gain access to exclusive content as soon as they subscribe, lose access if their renewal payment fails repeatedly, and have their access cleanly revoked when they cancel. The marketing team also needs the subscription to offer a 14-day free trial to reduce churn from hesitant buyers, with a monthly billing cycle of $29.99.
The dev team needs a WordPress plugin that both creates the subscription product programmatically (for consistent staging/production deployments) and wires up the key lifecycle events — activation, renewal success, renewal failure, and cancellation — so that user roles and access are managed automatically without manual admin intervention.
Output Specification
Write a single PHP plugin file at premium-membership-plugin.php that:
1. Creates the Premium Membership subscription product programmatically (to run once on activation or via a setup function). 2. Hooks into the relevant subscription lifecycle events to manage a custom WordPress user role called premium_member. 3. On repeated renewal failures (2 or more), the plugin should take additional protective action beyond just removing the role.
The plugin file should be self-contained and include all necessary add_action / add_filter calls. Include inline comments explaining each major section.
{
"name": "finsi/woocommerce-subscriptions",
"version": "0.1.0",
"summary": "Recurring payments and subscription product types in WooCommerce",
"skills": {
"woocommerce-subscriptions": {
"path": "SKILL.md"
}
}
}