2026年6月21日
2026年6月21日
WooCommerceで定期購入(サブスクリプション)を実装する方法:標準機能とフックでの拡張
WooCommerce で月額・年額の定期購入(サブスクリプション)を扱うには、公式の有料拡張機能 WooCommerce Subscriptions を使うのが一般的です。自動更新の課金、無料トライアル、更新前のお知らせメール、プランの切り替えと按分は Subscriptions が標準で持っているので、同じ処理を自作すると二重に動いて不整合の原因になります。
この記事では、まず Subscriptions の標準機能でできることを整理し、そのうえで標準に無い処理(会員サイトの権限の同期、外部システムへの通知、トライアル終了時の記録)をフックで足す方法を説明します。内容は 2026年10月時点の WooCommerce Subscriptions の公式ドキュメントに基づきます。設定の名前は英語の画面表記で書いています。
始める前に確認すること
- 決済方法が自動の継続課金に対応しているか:自動で更新料金を請求するには、決済ゲートウェイが Subscriptions の自動更新に対応している必要があります。対応していない決済方法では、顧客が更新のたびに手動で支払う形になります。使う予定の決済プラグインの説明で、Subscriptions 対応かを確認してください。
- テスト環境の扱い:Subscriptions は、本番サイトを複製したステージング環境を検出すると、自動更新の課金を止めます。更新前のお知らせメールも本番以外の環境では送られません。ステージングでは、決済ゲートウェイのテストモードと組み合わせて動作を確認します。
- バックアップ:定期購入の注文や状態はデータベースに保存されます。設定やコードを本番に入れる前に、データベースのバックアップを取ります。
標準機能でできること
| やりたいこと | Subscriptions の標準機能(英語表記) |
|---|---|
| 月額・年額などの周期で課金する | Frequency(数値と Day / Week / Month / Year) |
| 決まった回数で終わらせる | Expire after a set number of payments |
| 初回だけ手数料を取る | Signup Fee |
| 無料トライアル | Free Trial(数値と期間) |
| トライアル終了や更新の前にお知らせメールを送る | Subscription notifications(Send reminder emails to subscribers、Reminder timing) |
| 契約中のユーザーに権限(ロール)を付ける | Subscriber roles(Subscriber default role、Inactive subscriber role) |
| プランの変更(アップグレード・ダウングレード)と按分 | Switching、First billing behavior |
| 支払い失敗時の自動再試行 | Payment recovery |
公式ドキュメントでは、バージョン 9.0 以降、WooCommerce → 設定 → Subscriptions の Storewide Subscription Plans で再利用できるプランを作り、通常の商品(単純な商品、バリエーションのある商品など)に付ける方法が勧められています。従来の Simple subscription・Variable subscription という商品タイプも、設定で有効にすれば使えます。
トライアル終了前のお知らせは標準機能で送る
トライアルの終了前に顧客へメールを送りたい場合は、自作のコードではなく標準のお知らせ機能を使います(WooCommerce Subscriptions 6.9.0 以降)。
- WooCommerce → 設定 → Subscriptions の Subscription notifications で、Send reminder emails to subscribers を有効にし、Reminder timing で何日前に送るかを決めます。
- WooCommerce → 設定 → メールで、送りたいお知らせのメール(自動支払いのトライアル終了、手動支払いのトライアル終了、自動更新、手動更新、期限切れ)をそれぞれ有効にします。
公式ドキュメントによると、お知らせは最も遅くて対象日の1日前に送られ、期間が3日未満の定期購入には送られません。対象は状態が active または pending-cancel の定期購入です。
権限の付与もまず標準機能で
会員向けのページを契約者だけに見せたい場合、Subscriber roles の設定で、有効な定期購入があるユーザーのロール(既定は WordPress の「購読者」)と、無くなったときのロール(既定は WooCommerce の「顧客」)が自動で切り替わります。独自のロールを付け外しするコードを別に書くと、この自動切り替えとぶつかります。
フックで標準に無い処理を足す
ここからは、標準機能に無い処理を足す例です。コードは独自のプラグイン(wp-content/plugins/my-subscription-integration/my-subscription-integration.php など)にまとめ、ステージング環境で動作を確認してから本番に入れます。
使うフックは次の3つです。
| フック | 引数 | 動くタイミング |
|---|---|---|
woocommerce_subscription_status_updated |
定期購入オブジェクト、新しい状態、前の状態 | 状態が変わった直後(理由を問わない) |
woocommerce_subscription_renewal_payment_complete |
定期購入オブジェクト、更新の注文 | 更新の支払いが成功した後(自動・手動とも。初回の注文と切り替えの注文では動かない) |
woocommerce_scheduled_subscription_trial_end |
定期購入の ID | トライアルの終了日になったとき(終了前ではない) |
状態には pending、active、on-hold、pending-cancel、cancelled、expired があります。特定の状態だけを扱う場合は woocommerce_subscription_status_active のような状態名つきのフックも使えます。
<?php
/**
* Plugin Name: My Subscription Integration
*/
// 1. 有効な定期購入があるかをユーザーメタに反映する
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;
}
// 同じユーザーの他の定期購入も含めて判定する(1件の解約で他の契約分まで外さない)
$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. 更新(継続課金)の支払いが成功したら、外部システムへの通知を予約する
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'
);
}
// 予約した通知をバックグラウンドで送る
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(), // 受信側はこの値で二重処理を防ぐ
'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 ) {
// 例外を投げると、スケジュール済みアクションの画面に失敗として記録される
throw new Exception( 'Renewal webhook failed for order ' . $order_id );
}
}
// 3. 無料トライアルが終わった時点で、自社の記録を更新する
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).' );
}
コードの要点
- 1. 権限フラグの同期:状態ごとに付け外しするのではなく、変化があるたびに
wcs_user_has_subscription()で「有効な定期購入が1件でも残っているか」を調べ直しています。1人が複数の定期購入を持っている場合に、1件の解約で他の契約分の権限まで外すことを防げます。pending-cancel(解約手続き済みで、支払い済み期間の終わりを待っている状態)も、期間が終わるまでは利用できる扱いにしています。会員ページの表示を切り替えるときは、このユーザーメタを見ます。 - 2. 外部システムへの通知:支払いのフックの中で直接 HTTP リクエストを送ると、相手の応答が遅いときに更新処理全体が遅れます。そこで WooCommerce に同梱されている Action Scheduler の
as_enqueue_async_action()で送信を予約し、バックグラウンドで送っています。送信に失敗すると例外を投げ、WooCommerce → ステータス → 予約済みアクション(英語表示では Scheduled Actions)に失敗として記録されます。公式ドキュメントでは、このフックの中で次回の支払日を読まないよう注意されているので、通知には含めていません。 - 3. トライアル終了時の処理:
woocommerce_scheduled_subscription_trial_endは、トライアルの終了日になった時点で動きます。「終了の3日前」のような事前のお知らせには使えないので、顧客へのお知らせは前の節の標準機能で送り、このフックでは自社の記録の更新など、終了時点で行う処理だけを書きます。
同じ状態変化のフックが2回呼ばれても結果が変わらないように書いておくと(上の例では、毎回調べ直して同じ値を保存するだけ)、再試行や手動での状態変更があっても安全です。
受信側で署名を確かめる
通知を受け取る外部システムでは、送信側と同じ秘密鍵で HMAC を計算し、X-Signature ヘッダーと一致するかを確かめてから処理します。秘密鍵は推測できない長い文字列にし、通知の URL とともに wp-config.php やオプションに保存して、コードに直接書かないようにします。
<?php
// 受信側(外部システム)での署名の確認例
$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 );
// 古い通知の再送を受け付けない(5分以上前のものは拒否)
if ( abs( time() - (int) $data['timestamp'] ) > 300 ) {
http_response_code( 400 );
exit;
}
// ここで order_id を使って処理済みかを確かめ、未処理なら反映する
http_response_code( 200 );
文字列の比較に === ではなく hash_equals() を使うのは、比較にかかる時間から署名を推測されるのを防ぐためです。
動作を確認する
- ステージング環境で、決済ゲートウェイをテストモードにして、トライアル付きのプランと付いていないプランを購入します。
- WooCommerce → ステータス → 予約済みアクションで、
woocommerce_scheduled_subscription_で始まるアクション(更新の支払い、トライアル終了など)が予定どおり登録されているかを確認します。予定日を待たずに試す場合は、この画面から対象のアクションを実行できます。 - 管理画面で定期購入の状態を手動で変え(保留、キャンセルなど)、ユーザーメタ
myplugin_has_accessが期待どおりに変わるかを確認します。 - 更新の支払いを成功させ、予約済みアクションに
myplugin_send_renewal_webhookが登録・実行されること、受信側で署名の確認が通ることを確認します。
まとめ
- WooCommerce の定期購入は、公式の WooCommerce Subscriptions を使い、課金・トライアル・お知らせ・ロール・切り替えと按分は標準機能で設定します。同じ処理を自作しないことが、不整合を防ぐ近道です。
- 標準に無い処理は、
woocommerce_subscription_status_updated、woocommerce_subscription_renewal_payment_complete、woocommerce_scheduled_subscription_trial_endなどのフックで足します。 - トライアル終了のフックは終了時点で動くので、事前のお知らせは標準のお知らせ機能(6.9.0 以降)で送ります。
- 外部への通知は Action Scheduler で非同期に送り、HMAC の署名と注文 ID で、なりすましと二重処理を防ぎます。