WooCommerce customers and sessions
Use this skill whenever code touches the active shopper, account profile, cart-flow state, or Store API session.
The critical distinction
WC()->customer is created in session mode:
new WC_Customer( get_current_user_id(), true );
Its data store is WC_Customer_Data_Store_Session. Calling WC()->customer->save() updates the WooCommerce session for both guests and logged-in users; it does not persist account fields to user meta.
To edit a registered customer's durable profile, load a separate non-session object:
$customer = new WC_Customer( $user_id );
$customer->set_billing_phone( $phone );
$customer->save();
Never use the active session object as a shortcut for an account-profile write.
WC_Customer::get_meta_data() is not a safe way to publish a user's entire metadata bag. WooCommerce filters its own customer model's internal/account-preference keys (WooCommerce 11.0 also excludes WordPress's infinite_scrolling preference), but arbitrary third-party user meta can still be private. REST responses, exports, and headless profiles should use an explicit allowlist of extension-owned keys rather than forwarding customer meta wholesale.
Initialization boundaries
On normal cart and checkout requests WooCommerce initializes WC()->session, WC()->customer, and WC()->cart. They are not guaranteed in early hooks, WP-CLI, cron, arbitrary REST routes, or admin requests.
add_action( 'wp_loaded', static function (): void {
if ( ! function_exists( 'WC' ) || ! WC()->session ) {
return;
}
$campaign = WC()->session->get( 'myplugin_campaign', '' );
} );
Do not call WC()->initialize_cart() globally just to make a property non-null. Initialize cart/session state only in a request that genuinely needs shopper state.
Store plugin state in the session
add_action( 'wp_loaded', static function (): void {
if ( ! WC()->session ) {
return;
}
WC()->session->set(
'myplugin_quote',
array(
'product_id' => absint( $_POST['product_id'] ?? 0 ),
'quantity' => max( 1, absint( $_POST['quantity'] ?? 1 ) ),
)
);
// Must happen before headers are sent. set() marks data dirty; the DB row
// is written later by save_data(), normally during shutdown.
WC()->session->set_customer_session_cookie( true );
} );
Rules:
- Namespace keys, for example
myplugin_quote; never reuse core keys such ascart,customer, ororder_awaiting_payment. - Store small scalar/array data. Although values are serialized, avoid service objects, Woo objects, closures, and credentials.
- Treat session values as untrusted input when they reach a write, payment, or authorization decision.
set_customer_session_cookie( true )sets the cookie/flag only. It does not itself create awp_woocommerce_sessionsrow.- A row is persisted only when the session is dirty and has a session identity.
- Set cookies before output; calling the method after headers were sent cannot repair the response.
Read and remove state
$quote = WC()->session ? WC()->session->get( 'myplugin_quote', array() ) : array();
if ( WC()->session ) {
WC()->session->__unset( 'myplugin_quote' );
}
Do not use PHP's native $_SESSION. WooCommerce intentionally owns its session lifecycle and storage.
Checkout customer vs account profile
Use the session customer for request-local tax, address, and checkout calculations:
if ( WC()->customer ) {
WC()->customer->set_billing_country( 'HU' );
WC()->customer->set_shipping_postcode( '1051' );
WC()->customer->save(); // session only
}
For durable account updates, verify authorization and use a fresh persistent customer:
if ( get_current_user_id() !== $user_id && ! current_user_can( 'edit_users' ) ) {
return;
}
$customer = new WC_Customer( $user_id );
$customer->set_billing_country( $country );
$customer->save();
Guest login migration
The classic session handler migrates guest state to the logged-in customer and removes the old guest row when saving. Do not manually copy the whole serialized session into user meta. If plugin state should survive login, keep it under a namespaced session key and test the login transition.
If data should survive across devices or after session expiry, it is not session data. Store it in an owned table, user meta, order meta, or another durable model with an explicit retention policy.
WooCommerce 11.0 account email verification and past guest orders
WooCommerce 11.0 no longer treats creating or logging into an account as sufficient proof for claiming matching historical guest orders. Core links those orders through wc_update_new_customer_past_orders() only after the customer's current account email has been verified.
Stable extension points:
add_action( 'woocommerce_customer_email_verified', static function ( int $user_id ): void {
// Observe the verified transition; core also links matching guest orders here.
} );
add_action(
'woocommerce_customer_verify_email_notification',
static function ( int $user_id, string $verify_url ): void {
// Observe or integrate with delivery. Treat $verify_url as a secret, one-time bearer URL.
},
10,
2
);
Rules:
- Do not call
wc_update_new_customer_past_orders()early merely because an account email matches an order. That recreates the account-takeover/privacy boundary WooCommerce 11.0 tightened. - Do not read or write
_wc_email_verifiedor_wc_email_verification_key; these are internal storage details, not extension contracts. - Do not import classes from
Automattic\WooCommerce\Internal\CustomerEmailVerification. Observe the two public hooks above. - The verified state is bound to the current account email and becomes invalid when that address changes.
- Never log, persist in analytics, or expose
$verify_url; core verification keys are single-use and time-limited.
Classic cookie sessions and Store API Cart-Token
These are related but not interchangeable:
| Surface | Identity | Storage behavior |
|---|---|---|
| Classic frontend | wp_woocommerce_session_* cookie |
WC_Session_Handler; DB table plus object-cache layer and cleanup cron |
| Store API with valid token | Cart-Token header |
StoreApi\SessionHandler; same DB table, no cookie, cron, or cache layer |
Do not parse or mint Cart-Tokens yourself. Let Store API return a Cart-Token response header and send it back on later cart requests.
WooCommerce 10.9 can also initialize the classic handler from a valid ?session=<Cart-Token> query value and clone guest data. Treat Cart-Tokens as bearer credentials: do not log them, place them in analytics URLs, or expose them to unrelated origins.
Empty-session cleanup
The experimental destroy-empty-sessions feature is disabled by default in WooCommerce 11.0. When enabled, WooCommerce may remove an empty guest session and cookie to improve page caching. Plugins must not depend on the mere existence of a session cookie; store actual namespaced data when continuity is required.
Do not force empty cookies as a tracking or cache-bypass mechanism.
Security and concurrency
- A session identifies cart state; it is not an authorization system.
- Validate nonce/authentication, capability, ownership, and business rules at every write endpoint.
- Avoid read-modify-write counters in sessions across parallel AJAX requests; last write can win.
- Never store PAN, CVV, passwords, API secrets, or bearer tokens in session data.
- Regenerate/transition through WooCommerce's own login and logout flow; do not replace session IDs manually.
Common mistakes
// WRONG: this only changes the active checkout session.
WC()->customer->set_billing_phone( $phone );
WC()->customer->save();
// RIGHT: persistent registered-customer update.
$customer = new WC_Customer( $user_id );
$customer->set_billing_phone( $phone );
$customer->save();
// WRONG: cookie creation is not a DB write and this stores no state.
WC()->session->set_customer_session_cookie( true );
// RIGHT: set namespaced data, then ensure the cookie before headers.
WC()->session->set( 'myplugin_context', array( 'source' => 'quote' ) );
WC()->session->set_customer_session_cookie( true );
Cross-references
wc-store-apifor Nonce/Cart-Token request handling.wc-cart-checkout-classicfor cart-item and checkout persistence.wc-payment-tokensfor durable provider-token records; payment tokens do not belong in sessions.
References
WC_Customerconstructor and session flag:includes/class-wc-customer.php.- Session-only customer data store:
includes/data-stores/class-wc-customer-data-store-session.php. - Cookie, dirty-write, Cart-Token clone, and empty cleanup behavior:
includes/class-wc-session-handler.php. - Header-based Store API handler:
src/StoreApi/SessionHandler.php. - Account verification and verified guest-order linking:
src/Internal/CustomerEmailVerification/*. - Official documentation: https://woocommerce.github.io/code-reference/classes/WC-Customer.html
- Verified source paths:
wp-content/plugins/woocommerce/includes/abstracts/abstract-wc-session.phpwp-content/plugins/woocommerce/src/Internal/Features/FeaturesController.php