2026年11月12日
2026年11月12日
WooCommerce Subscriptions: Built-in Features and Extending with Hooks
This article is a localized edition of the Japanese original. Read the Japanese original
To sell monthly or yearly subscriptions with WooCommerce, the usual approach is the official paid extension WooCommerce Subscriptions. Subscriptions already handles automatic renewal billing, free trials, reminder emails before renewal, and plan switching with proration, so building the same logic yourself means it runs twice and causes inconsistencies.
This article first sorts out what Subscriptions can do out of the box, then shows how to add what it does not cover (syncing access on a membership site, notifying an external system, recording the end of a trial) with hooks. It is based on the official WooCommerce Subscriptions documentation as of October 2026. Setting names are given as they appear in the English admin screens.
Check before you start
- Does your payment method support automatic recurring billing?: To charge renewals automatically, the payment gateway must support automatic renewals with Subscriptions. With payment methods that don't, customers pay manually at each renewal. Check the description of the payment plugin you plan to use to confirm that it supports Subscriptions.
- How test environments are handled: When Subscriptions detects a staging site that is a copy of the live site, it stops charging automatic renewals. Reminder emails are not sent outside production either. On staging, check behavior together with your payment gateway's test mode.
- Backups: Subscription orders and statuses are stored in the database. Back up the database before putting settings or code into production.
What the built-in features cover
| What you want to do | Built-in Subscriptions feature (English labels) |
|---|---|
| Bill on a cycle such as monthly or yearly | Frequency (a number plus Day / Week / Month / Year) |
| End after a fixed number of payments | Expire after a set number of payments |
| Charge a fee on the first payment only | Signup Fee |
| Free trial | Free Trial (a number plus a period) |
| Email customers before a trial ends or a renewal | Subscription notifications (Send reminder emails to subscribers, Reminder timing) |
| Give subscribers a role | Subscriber roles (Subscriber default role, Inactive subscriber role) |
| Plan changes (upgrades and downgrades) with proration | Switching, First billing behavior |
| Automatic retries of failed payments | Payment recovery |
The official documentation recommends that, from version 9.0, you create reusable plans under Storewide Subscription Plans in WooCommerce > Settings > Subscriptions and attach them to regular products (simple products, variable products and so on). The older Simple subscription and Variable subscription product types are still available if you enable them in the settings.
Send pre-trial-end reminders with the built-in feature
If you want to email customers before their trial ends, use the built-in reminder feature rather than your own code (WooCommerce Subscriptions 6.9.0 or later).
- In WooCommerce > Settings > Subscriptions, under Subscription notifications, enable Send reminder emails to subscribers and use Reminder timing to set how far in advance to send.
- In WooCommerce > Settings > Emails, enable each reminder email you want to send (trial ending with automatic payment, trial ending with manual payment, automatic renewal, manual renewal, expiration).
According to the official documentation, reminders are sent no later than 1 day before the event, and they are not sent for subscriptions shorter than 3 days. They apply to subscriptions whose status is active or pending-cancel.
Use the built-in feature for access roles first
If you want to show members-only pages to subscribers only, the Subscriber roles setting switches roles automatically: a role for users with an active subscription (by default WordPress's Subscriber) and a role for when they no longer have one (by default WooCommerce's Customer). Writing separate code that adds and removes your own roles will clash with this automatic switching.
Add what the built-in features don’t cover with hooks
The following are examples of adding behavior that is not built in. Put the code in your own plugin (for example wp-content/plugins/my-subscription-integration/my-subscription-integration.php), test it on staging, and only then deploy it to production.
These three hooks are used:
| Hook | Arguments | When it runs |
|---|---|---|
woocommerce_subscription_status_updated |
Subscription object, new status, previous status | Right after the status changes (for any reason) |
woocommerce_subscription_renewal_payment_complete |
Subscription object, renewal order | After a renewal payment succeeds (automatic or manual; not for the initial order or switch orders) |
woocommerce_scheduled_subscription_trial_end |
Subscription ID | When the trial end date arrives (not before) |
Statuses are pending, active, on-hold, pending-cancel, cancelled and expired. To handle one status only, you can also use status-specific hooks such as woocommerce_subscription_status_active.
<?php
/**
* Plugin Name: My Subscription Integration
*/
// 1. Store in user meta whether the user has an active subscription
add_action( 'woocommerce_subscription_status_updated', 'myplugin_sync_access_flag', 10, 3 );
function myplugin_sync_access_flag( $subscription, $new_status, $old_status ) {
$user_id = $subscription->get_user_id();
if ( ! $user_id ) {
return;
}
// Check all of the user's subscriptions (one cancellation must not remove access granted by another)
$has_access = wcs_user_has_subscription( $user_id, '', array( 'active', 'pending-cancel' ) );
update_user_meta( $user_id, 'myplugin_has_access', $has_access ? 1 : 0 );
}
// 2. When a renewal payment succeeds, queue a notification to the external system
add_action( 'woocommerce_subscription_renewal_payment_complete', 'myplugin_queue_renewal_webhook', 10, 2 );
function myplugin_queue_renewal_webhook( $subscription, $last_order ) {
as_enqueue_async_action(
'myplugin_send_renewal_webhook',
array( $subscription->get_id(), $last_order->get_id() ),
'myplugin'
);
}
// Send the queued notification in the background
add_action( 'myplugin_send_renewal_webhook', 'myplugin_send_renewal_webhook', 10, 2 );
function myplugin_send_renewal_webhook( $subscription_id, $order_id ) {
$url = get_option( 'myplugin_webhook_url' );
$secret = get_option( 'myplugin_webhook_secret' );
$order = wc_get_order( $order_id );
if ( ! $url || ! $secret || ! $order ) {
return;
}
$body = wp_json_encode( array(
'event' => 'subscription.renewed',
'subscription_id' => (int) $subscription_id,
'order_id' => $order->get_id(), // The receiver uses this to avoid processing twice
'total' => $order->get_total(),
'currency' => $order->get_currency(),
'timestamp' => time(),
) );
$response = wp_remote_post( $url, array(
'timeout' => 10,
'headers' => array(
'Content-Type' => 'application/json',
'X-Signature' => hash_hmac( 'sha256', $body, $secret ),
),
'body' => $body,
) );
$code = wp_remote_retrieve_response_code( $response );
if ( is_wp_error( $response ) || $code < 200 || $code >= 300 ) {
// Throwing an exception records the action as failed on the Scheduled Actions screen
throw new Exception( 'Renewal webhook failed for order ' . $order_id );
}
}
// 3. Update your own records when the free trial ends
add_action( 'woocommerce_scheduled_subscription_trial_end', 'myplugin_on_trial_end', 10, 1 );
function myplugin_on_trial_end( $subscription_id ) {
$subscription = wcs_get_subscription( $subscription_id );
if ( ! $subscription ) {
return;
}
update_user_meta( $subscription->get_user_id(), 'myplugin_trial_ended_at', time() );
$subscription->add_order_note( 'Trial ended (recorded by My Subscription Integration).' );
}
Key points of the code
- 1. Syncing the access flag: Instead of granting and revoking per status, every change re-checks with
wcs_user_has_subscription()whether at least one active subscription remains. When one person has several subscriptions, this prevents a single cancellation from revoking access that another subscription still grants.pending-cancel(cancelled, waiting for the end of the paid period) is also treated as having access until the period ends. Check this user meta when you switch what members-only pages show. - 2. Notifying an external system: Sending an HTTP request directly inside the payment hook delays the whole renewal when the other side responds slowly. So the code uses
as_enqueue_async_action()from Action Scheduler, which ships with WooCommerce, to queue the send and run it in the background. If sending fails it throws an exception, and the action is recorded as failed under WooCommerce > Status > Scheduled Actions. The official documentation warns against reading the next payment date inside this hook, so the notification does not include it. - 3. Handling the end of a trial:
woocommerce_scheduled_subscription_trial_endruns when the trial end date arrives. It cannot be used for advance notices such as "3 days before the end", so send customer reminders with the built-in feature from the previous section and use this hook only for things that happen at the end, such as updating your own records.
If you write the code so that running the same status-change hook twice gives the same result (in the example, it simply re-checks and saves the same value each time), retries and manual status changes are safe.
Verify the signature on the receiving side
The external system that receives the notification computes the HMAC with the same secret key as the sender and checks that it matches the X-Signature header before processing. Make the secret a long string that cannot be guessed, store it together with the notification URL in wp-config.php or an option, and do not hard-code it.
<?php
// Example of verifying the signature on the receiving (external) system
$secret = getenv( 'WEBHOOK_SECRET' );
$body = file_get_contents( 'php://input' );
$signature = isset( $_SERVER['HTTP_X_SIGNATURE'] ) ? $_SERVER['HTTP_X_SIGNATURE'] : '';
if ( ! hash_equals( hash_hmac( 'sha256', $body, $secret ), $signature ) ) {
http_response_code( 401 );
exit;
}
$data = json_decode( $body, true );
// Reject replays of old notifications (anything older than 5 minutes)
if ( abs( time() - (int) $data['timestamp'] ) > 300 ) {
http_response_code( 400 );
exit;
}
// Use order_id here to check whether it was already processed; apply it if not
http_response_code( 200 );
The comparison uses hash_equals() instead of === to prevent attackers from guessing the signature from how long the comparison takes.
Test the behavior
- On staging, put the payment gateway in test mode and buy a plan with a trial and a plan without one.
- Under WooCommerce > Status > Scheduled Actions, check that actions starting with
woocommerce_scheduled_subscription_(renewal payment, trial end and so on) are registered as expected. To test without waiting for the scheduled date, you can run the action from this screen. - Change a subscription's status manually in the admin (on hold, cancelled and so on) and check that the user meta
myplugin_has_accesschanges as expected. - Complete a renewal payment and check that
myplugin_send_renewal_webhookis registered and run under Scheduled Actions, and that the signature check passes on the receiving side.
Summary
- Build WooCommerce subscriptions on the official WooCommerce Subscriptions extension, and configure billing, trials, reminders, roles, switching and proration with the built-in features. Not reimplementing them yourself is the shortest way to avoid inconsistencies.
- Add what is not built in with hooks such as
woocommerce_subscription_status_updated,woocommerce_subscription_renewal_payment_completeandwoocommerce_scheduled_subscription_trial_end. - The trial-end hook runs at the moment the trial ends, so send advance notices with the built-in reminder feature (6.9.0 or later).
- Send external notifications asynchronously with Action Scheduler, and use an HMAC signature and the order ID to prevent spoofing and double processing.
Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)