WooCommerce Subscriptions: data model, switching, gifting
Use this when exact storage names or switch/gift internals matter. Prefer WCS CRUD functions and WC_Subscription methods for writes; use raw keys only for audits, migrations, debugging, or compatibility glue.
Core names
| Entity | Name/key | Notes |
|---|---|---|
| Subscription order type | shop_subscription |
Registered with wc_register_order_type(), class WC_Subscription. In CPT storage it is a post type; in HPOS it is an order type. |
| Simple subscription product type | subscription |
WC_Product_Subscription::get_type(). |
| Variable subscription product type | variable-subscription |
WC_Product_Variable_Subscription::get_type(). |
| Subscription variation product type | subscription_variation |
WC_Product_Subscription_Variation::get_type(). |
| Action Scheduler group | wc_subscription_scheduled_event |
Used for subscription scheduled events. |
Subscription statuses are WooCommerce order statuses with wc- prefix in storage: wc-pending, wc-active, wc-on-hold, wc-cancelled, wc-switched, wc-expired, and wc-pending-cancel. Object APIs usually use unprefixed values.
Subscription meta keys
These keys map to WC_Subscription props in both CPT and HPOS subscription data stores.
| Meta key | Prop/date key | Purpose |
|---|---|---|
_billing_period |
billing_period |
day, week, month, or year. |
_billing_interval |
billing_interval |
Billing interval integer. |
_suspension_count |
suspension_count |
Customer/admin suspension count. |
_cancelled_email_sent |
cancelled_email_sent |
Cancellation email guard. |
_requires_manual_renewal |
requires_manual_renewal |
Manual renewal flag. |
_trial_period |
trial_period |
Trial period unit. |
_last_order_date_created |
last_order_date_created |
Last related parent/renewal order created date used by WCS. |
_schedule_start |
start / schedule_start |
Subscription start datetime. |
_schedule_trial_end |
trial_end / schedule_trial_end |
Trial end datetime. |
_schedule_next_payment |
next_payment / schedule_next_payment |
Next renewal datetime. |
_schedule_cancelled |
cancelled / schedule_cancelled |
Cancellation datetime. |
_schedule_end |
end / schedule_end |
End/expiration datetime. |
_schedule_payment_retry |
payment_retry / schedule_payment_retry |
Retry datetime. |
_subscription_switch_data |
switch_data |
Switch order execution payload. |
Use WC_Subscription::update_dates(), delete_date(), setters like set_billing_period(), and save(). Directly updating these meta keys can desync validation and scheduled actions.
Product subscription meta
Subscription product data lives on product or product_variation posts.
| Meta key | Purpose |
|---|---|
_subscription_price |
Recurring price. |
_subscription_sign_up_fee |
Sign-up fee. |
_subscription_period |
Billing period. |
_subscription_period_interval |
Billing interval. |
_subscription_length |
Subscription length. |
_subscription_trial_period |
Trial period unit. |
_subscription_trial_length |
Trial length. |
_subscription_gifting |
Product-level gifting override: enabled, disabled, or empty for global setting. |
_subscription_one_time_shipping |
One-time shipping flag. |
_subscription_payment_sync_date |
Renewal synchronization setting. |
Use WC_Subscriptions_Product helpers such as get_price(), get_period(), get_interval(), get_length(), get_trial_length(), get_sign_up_fee(), and get_gifting().
APFS / Subscription Plans storage in WCS 9.0+
All Products for Subscriptions is bundled into WooCommerce Subscriptions 9.0 as Subscription Plans. It can make ordinary product types behave as subscriptions through runtime meta and woocommerce_is_subscription.
| Storage | Key | Purpose |
|---|---|---|
| Option | wcsatt_subscribe_to_cart_schemes |
Storewide plan definitions. |
| Product meta | _wcsatt_schemes_status |
Product purchase mode: disable, override, or inherit. |
| Product meta | _wcsatt_schemes |
Product-specific custom plans in override mode. |
| Product meta | _wcsatt_storewide_selection_mode |
all or specific storewide plan selection. |
| Product meta | _wcsatt_selected_storewide_plans |
Storewide plan IDs allowed for a specific product. |
| Product meta | _wcsatt_force_subscription |
yes means disable one-time purchase when plans are active. |
| Product meta | _wcsatt_disabled |
Legacy one-time-only flag maintained for compatibility. |
| Cart item data | wcsatt_data.active_subscription_scheme |
Selected plan key, false for one-time, null for undefined/default. |
| Order item meta | _wcsatt_scheme |
Selected plan key persisted on order/subscription line items. |
| Order item meta | _wcsatt_scheme_id |
Legacy APFS plan key. |
Use WCS_ATT_Product::get_subscription_scheme_mode(), WCS_ATT_Product::set_subscription_scheme_mode(), WCS_ATT_Product_Schemes::get_subscription_schemes(), WCS_ATT_Cart::get_subscription_scheme(), and WCS_ATT_Order::get_subscription_scheme(). Do not infer APFS plans from the native _subscription_* product meta table above; APFS sets WCS-compatible values as runtime meta when a scheme is active.
Related order relation meta
WCS relates ordinary orders to subscriptions with order meta and relation stores. Do not infer relation from parent ID alone.
| Meta key | Relation |
|---|---|
_subscription_renewal |
Renewal order to subscription. |
_subscription_switch |
Switch order to subscription. |
_subscription_resubscribe |
Resubscribe order to subscription. |
Use wcs_get_subscription_ids_for_order(), wcs_get_subscriptions_for_order(), wcs_get_subscriptions_for_renewal_order(), wcs_get_subscriptions_for_switch_order(), and $subscription->get_related_orders().
Switcher flow
Switching replaces or adds subscription line items through checkout. It is not a simple product ID update.
- A user clicks the switch link printed in My Account by
WC_Subscriptions_Switcher::print_switch_link(). - The link points to the product/grouped product URL with
switch-subscription,item, and_wcsnoncequery args. subscription_switch_handler()validates ownership, nonce, and item switchability.validate_switch_request()blocks non-subscription products and identical product/variation/quantity switches.set_switch_details_in_cart()adds cart item data undersubscription_switch.WCS_Switch_Totals_Calculatorcalculates proration, switch direction, first payment timestamp, and possible prepaid-term changes.- Checkout line item meta records prorated amounts on the switch order and switched item links on subscription items.
process_checkout()creates_subscription_switch_dataon the switch order and may create pending switch items on the existing subscription.- When the switch order is paid/completed,
complete_subscription_switches()applies the payload to the subscription. - WCS fires
woocommerce_subscriptions_switch_completedand item-level switch actions after completion.
Custom switch flows
WCS does not expose a simple customer REST endpoint for "switch this subscription item to that product". The built-in switcher is a cart/checkout/order-completion flow. Custom account UI, AJAX, or REST layers must wrap that flow or deliberately reproduce its order payload.
Safe pattern:
- Verify the current user can switch the exact subscription item with
WC_Subscriptions_Switcher::can_item_be_switched_by_user(). - Build a temporary cart/session context for the current user and add the replacement product with
subscription_switchcart item data. - Let
WCS_Switch_Totals_Calculatorcalculate prorations,first_payment_timestamp,end_timestamp,force_payment, and switch direction. - Return preview totals from the calculated cart/order data, not from hand-written price math.
- On confirmation, create the switch order with the same
_subscription_switch_datashape WCS checkout writes. - If an immediate payment is due, process it through the gateway/order payment flow; if no payment is due, complete/apply the switch through WCS completion logic.
- Let
complete_subscription_switches()apply the change and let WCS fire its normal hooks.
If a pending/failed switch order is reopened through its pay link, reproduce WCS 9.1's full guard set before rebuilding the cart: valid order key, pending/failed status, actual switch relation and payload, logged-in user, pay_for_order on the order, switch_shop_subscription on every referenced subscription, and a bidirectional match between relation results and every _subscription_switch_data entry. An order key or payment capability alone is insufficient.
Do not implement switching by directly calling $subscription->remove_item() and $subscription->add_product() from a customer action. That bypasses switch orders, prorations, tax/fee/coupon handling, old item archival, pending switch item types, cancellation of older unpaid switch orders, and completion hooks.
For read-only previews or eligibility checks, it is fine to expose custom endpoints that return allowed products, switchable item IDs, and WCS-calculated preview totals. For writes, prefer a service that uses WCS cart/checkout objects internally.
Switch payload and extension reference
Cart items use subscription_switch; switch orders persist _subscription_switch_data; staged/archive items use custom *_pending_switch, *_switched, and line_item_removed types. These payloads include item IDs, proration results, schedule changes, coupons, fees, and shipping lines and are not a stable shortcut for direct mutation.
Use switching-reference.md for exact payload shapes, item meta, and hook signatures. In WCS 8.8+, wcs_switch_proration_extra_to_pay has a fifth $switch_item argument; register accepted args 5 when that context matters.
HPOS-safe order/subscription data copying
WCS copies parent, subscription, renewal, and resubscribe data through WC_Subscriptions_Data_Copier and filters such as wc_subscriptions_subscription_data, wc_subscriptions_renewal_order_data, and wc_subscriptions_object_data.
In WCS 9.1, values returned by those filters on HPOS are copied without an additional maybe_unserialize() pass; CPT/raw-storage values are decoded after the filters run. Return the PHP type expected by the destination setter. Do not pre-serialize arrays or objects merely because legacy postmeta stores serialized text, and do not assume an intentionally serialized string will be decoded twice on HPOS.
Regression-test the same copy callback with HPOS enabled and disabled, especially when a text field is itself a valid serialized-looking string.
Trash and restore contract in WCS 9.1
Treat trash/restore as an order lifecycle operation, not as a raw post-status edit.
- Restoring a parent order restores its trashed child subscriptions under both HPOS and legacy CPT storage.
- On CPT storage, WCS 9.1 restores a subscription to the recorded
_wp_trash_meta_statusinstead of WordPress's defaultdraftstatus, whichWC_Subscriptionwould otherwise expose aspending. - WCS normalizes unsafe pre-trash statuses to a restorable cancelled/pending/expired status before trashing. Do not overwrite
_wp_trash_meta_statusyourself. - CPT cache repair on
untrashed_postreads prefixed subscription meta such as_customer_userdirectly when generic object-property lookup cannot resolve it. Bypassingwp_untrash_post()can leave customer/subscription caches stale and keep the restored subscription out of My Account.
Use $subscription->delete( false ) / the WooCommerce data-store trash and untrash paths for programmatic lifecycle changes. Test both HPOS and CPT storage when an integration observes trash hooks, relation caches, or My Account visibility.
Gifting storage
Gifting is included in Subscriptions. It lets purchaser and recipient differ.
| Storage | Key | Purpose |
|---|---|---|
| Cart item data | wcsg_gift_recipients_email |
Recipient email before checkout/subscription creation. |
| Subscription meta | _recipient_user_email_address |
Recipient email captured at subscription creation before a user is resolved. |
| Subscription meta | _recipient_user |
Recipient user ID after account lookup/creation. Primary gifted-subscription marker. |
| Parent order item meta | _wcsg_cart_key |
Links checkout order item to the recurring cart/subscription item. |
| Parent order item meta | wcsg_recipient |
Value format wcsg_recipient_id_{user_id}. |
| Parent order item meta | wcsg_deleted_recipient_data |
JSON snapshot used after recipient deletion. |
| User meta | wcsg_update_account |
Recipient account setup/update flag. |
| User meta | wcsg_recipient_just_reset_password |
Recipient onboarding flag. |
Gifted subscriptions are detected by WCS_Gifting::is_gifted_subscription(): true when _recipient_user exists or _recipient_user_email_address is present.
Gifting flow
- Product page/cart/checkout collects recipient email when
WCSG_Product::is_giftable()is true. - Cart item stores
wcsg_gift_recipients_email; gifting is blocked for renewal and switch cart items. - Checkout uses recipient email in recurring cart keys so different recipients produce separate subscriptions.
woocommerce_checkout_subscription_createdwrites_recipient_user_email_addressto the subscription.- On parent order processing/completion, recipient management finds or creates the recipient user, writes
_recipient_user, addswcsg_recipientto the matching parent order item, and updates shipping fields from recipient user meta. - Recipients are granted view/pay/suspend/cancel capabilities for gifted subscriptions, but cannot change payment method.
_recipient_useris not copied to renewal orders.
Product giftability:
- Global enablement comes from WCSG admin settings.
- Product-level override is
_subscription_gifting:enabled,disabled, or empty for global. - In WCS 9.0 APFS, the Subscription Plans product panel can also write the product-level gifting override while saving ordinary products sold via plans.
- Variable subscription parent returns giftable to render UI; each variation decides final
giftingvariation data. - Product page gifting UI is suppressed during switching (
switch-subscriptionquery arg).
Memberships integration for gifts
If WooCommerce Memberships is active and a giftable subscription product grants a plan:
- Membership access is granted to the recipient when order item meta has
wcsg_recipient. - Purchaser access is skipped unless the same product was also purchased for the purchaser.
- The created
wc_user_membershipstill stores_subscription_id. - Because one order can contain the same product for multiple recipients, link the membership to the recipient's subscription, not just the first subscription in the order.
Common mistakes
// WRONG: direct date meta write can desync Action Scheduler.
update_post_meta( $subscription_id, '_schedule_next_payment', '2026-06-01 00:00:00' );
// RIGHT:
$subscription = wcs_get_subscription( $subscription_id );
$subscription->update_dates( array( 'next_payment' => '2026-06-01 00:00:00' ), 'gmt' );
// WRONG: treating a switch as a product meta update.
$subscription->remove_item( $old_item_id );
$subscription->add_product( wc_get_product( $new_product_id ) );
// RIGHT: use WCS switch flow or reproduce its order payload deliberately.
$is_switch = wcs_order_contains_switch( $order );
// WRONG: gifted subscription recipient is not the subscription customer.
$recipient_id = $subscription->get_user_id();
// RIGHT:
$recipient_id = WCS_Gifting::get_recipient_user( $subscription );
Cross-references
- Use
wcs-subscription-hooksfor general lifecycle hook selection. - Use
wcs-subscription-plans-apfsfor the full WCS 9.0 Subscription Plans / APFS plan API, REST endpoints, and headless cart behavior. - Use
wcs-renewal-schedulerfor renewal dates, Action Scheduler, and payment retry timing. - Use
wcs-subscription-downloadsfor linked downloadable products, download permission grants/revokes, and the subscription downloads mapping table. - Use
wcm-data-model-subscriptions-linkfor Memberships CPT/meta and the Memberships-to-Subscriptions relation.
References
- Official documentation: https://woocommerce.com/document/subscriptions/develop/
- Verified source paths:
wp-content/plugins/woocommerce-subscriptions/includes/core/class-wc-subscriptions-core-plugin.phpwp-content/plugins/woocommerce-subscriptions/includes/core/class-wc-subscription.phpwp-content/plugins/woocommerce-subscriptions/includes/core/data-stores/class-wcs-subscription-data-store-cpt.phpwp-content/plugins/woocommerce-subscriptions/includes/core/data-stores/class-wcs-orders-table-subscription-data-store.phpwp-content/plugins/woocommerce-subscriptions/includes/core/class-wc-subscriptions-product.phpwp-content/plugins/woocommerce-subscriptions/includes/core/wcs-functions.phpwp-content/plugins/woocommerce-subscriptions/includes/core/wcs-switch-functions.phpwp-content/plugins/woocommerce-subscriptions/includes/switching/class-wc-subscriptions-switcher.phpwp-content/plugins/woocommerce-subscriptions/includes/switching/class-wcs-cart-switch.phpwp-content/plugins/woocommerce-subscriptions/includes/switching/class-wcs-switch-totals-calculator.phpwp-content/plugins/woocommerce-subscriptions/includes/core/class-wc-subscriptions-data-copier.phpwp-content/plugins/woocommerce-subscriptions/includes/core/class-wc-subscriptions-manager.phpwp-content/plugins/woocommerce-subscriptions/includes/core/class-wcs-post-meta-cache-manager.phpwp-content/plugins/woocommerce-subscriptions/includes/downloads/wp-content/plugins/woocommerce-subscriptions/includes/apfs/class-wcs-att-product.phpwp-content/plugins/woocommerce-subscriptions/includes/apfs/product/class-wcs-att-product-schemes.phpwp-content/plugins/woocommerce-subscriptions/includes/apfs/class-wcs-att-cart.phpwp-content/plugins/woocommerce-subscriptions/includes/apfs/class-wcs-att-order.phpwp-content/plugins/woocommerce-subscriptions/includes/gifting/class-wcs-gifting.phpwp-content/plugins/woocommerce-subscriptions/includes/gifting/class-wcsg-product.phpwp-content/plugins/woocommerce-subscriptions/includes/gifting/class-wcsg-cart.phpwp-content/plugins/woocommerce-subscriptions/includes/gifting/class-wcsg-checkout.phpwp-content/plugins/woocommerce-subscriptions/includes/gifting/class-wcsg-recipient-management.php