Cartnorm B2B is not on WordPress.org yet. These pages describe version 0.3.0, the version that will be published. Pro features are marked as planned.
Store modes
Choose the mode in WooCommerce › Settings › B2B › Store mode & caching.
| Mode | Who sees prices | Who can buy |
|---|---|---|
| Hybrid (default) | Everyone. Consumers and guests see your normal WooCommerce prices; B2B groups see their own prices, net if the group is set to “excluding VAT”. | Everyone |
| B2B only | Only approved business customers (customers in a B2B group). Guests and customers without a group see no prices, also not in product blocks, the Store API or the structured data. | Only approved business customers |
In B2B only mode, other visitors see a notice with links to log in or to apply for a business account. A customer whose application is still under review is told so.
Restrict the catalog (B2B only mode): shows the shop, categories and products only to approved business customers. Other visitors are sent to the login page (My account) or the wholesale registration page, as you choose. Logged-in customers without a group always go to the registration page.
Legal note: when you may show only net prices
Showing only prices without VAT is allowed only if consumers are effectively excluded from buying, for example because every customer has to register with a VAT ID or company details that you check before you approve the account. A notice or a checkbox such as "I am a business customer" is not enough, and consumers who can still order must see prices including VAT. In Hybrid mode consumers see your normal prices, and only approved B2B groups see net prices. This is not legal advice; please ask your lawyer or tax adviser.
For a B2B-only shop this means: use B2B only mode, restrict the catalog, and use wholesale registration with manual approval, or automatic approval by VIES. Also check your other shop texts (for example consumer withdrawal information) with your lawyer, because a shop that addresses consumers can be treated as a consumer shop.
Deactivating the plugin makes your normal product prices visible to everyone again, because WooCommerce itself does not know the store mode. Do not deactivate Cartnorm B2B on a live B2B-only shop unless the shop is closed.
Caching
Why this matters
Cartnorm B2B can show prices excluding VAT to business customer groups. In Germany the price indication rules (PAngV) require prices shown to consumers to include VAT; other EU countries have similar rules. If a page cache saved a page that was rendered for a wholesale customer and later served that copy to a consumer, the consumer would see net prices. This is a legal risk for the shop, so Cartnorm B2B makes sure such pages are never cached and that caches which do cache logged-in users keep the groups apart.
What Cartnorm B2B does
Pages that are never cached
| Store mode | Pages that are never cached |
|---|---|
| Hybrid (default) | Every page for a logged-in customer in a B2B group |
| B2B only | Every page, for everyone (guests, customers without a group and group members) |
For these pages Cartnorm B2B
- sends
Cache-Control: private, no-store, max-age=0(plusPragma: no-cacheand anExpiresdate in the past), - defines the
DONOTCACHEPAGEconstant, which page cache plugins respect, - tells LiteSpeed Cache directly (
do_action( 'litespeed_control_set_nocache', … )), - answers Store API product requests (
/wc/store/v1/products…, used by product blocks) with the sameCache-Controlheader, - adds
<meta name="cartnorm-b2b-view" content="group">to the page head, so the self-test can recognise a group page that was cached.
Guests and customers without a group in Hybrid mode see your normal prices (gross, as configured in WooCommerce). Their pages stay cacheable, so your shop stays fast.
The group cookie cartnorm_b2b_group
Members of a group get a cookie named cartnorm_b2b_group. Its value is an HMAC (SHA-256, 32 hex characters) of the group slug and the group configuration, keyed with the site’s secret AUTH_KEY/AUTH_SALT. So:
- every member of the same group has the same value (a cache can share one copy per group),
- the value changes when you change the group settings (old cached copies are not used any more),
- it cannot be computed without your secret keys, and it does not contain the group name.
The cookie is never used to decide prices. Prices, net/gross display, order rules and VAT always come from the group stored on the server for the logged-in user. Setting or forging the cookie in the browser does not change any price. The cookie is HttpOnly, SameSite=Lax, Secure on HTTPS, and expires with the browser session. It is set on login, kept in line on every request, and removed on logout or when the customer leaves the group.
Cache plugins
Cartnorm B2B detects the plugins below and registers its integration only when the plugin is active. WooCommerce → Settings → B2B → Store mode & caching shows what was detected and what Cartnorm does.
WP Rocket
- Cartnorm adds
cartnorm_b2b_groupto WP Rocket’s dynamic cookies (rocket_cache_dynamic_cookies). If you enabled “Enable caching for logged-in WordPress users”, WP Rocket keeps one cache per cookie value (per group). - Cartnorm does not add a mandatory cookie (
rocket_cache_mandatory_cookies), because that would stop caching for all guests. - WP Rocket writes the cookie list into its config file. Cartnorm regenerates that file (
rocket_generate_config_file()) when WP Rocket is detected for the first time, when you save the “Store mode & caching” settings, and on deactivation (to remove the cookie again). - Pages for group members carry
DONOTCACHEPAGE, which WP Rocket respects (“DONOTCACHEPAGE is defined. Page cannot be cached.”). - Recommended: keep “Enable caching for logged-in WordPress users” off (default).
LiteSpeed Cache
litespeed_vary_cookies: the cookie is registered for the rewrite rules (.htaccess), so the LiteSpeed server varies by it.litespeed_vary_curr_cookies: the current response varies by the cookie when the visitor has it.litespeed_control_set_nocache: pages for group members (and all pages in “B2B only” mode) are marked not cacheable. LiteSpeed also honoursDONOTCACHEPAGE.- After installing Cartnorm B2B, LiteSpeed rewrites its rules the next time its settings are saved. Save LiteSpeed Cache → Cache once so the cookie is in
.htaccess. - Recommended: keep “Cache Logged-in Users” off. If you need it on, the vary cookie keeps groups apart.
W3 Total Cache
- W3 Total Cache does not store pages when
DONOTCACHEPAGEis defined (“DONOTCACHEPAGE defined”). - Recommended: keep Performance → Page Cache → “Don’t cache pages for logged in users” on (default).
- If you use “Disk: Enhanced”, W3TC serves cached files from
.htaccessrules for visitors without the WordPress login cookie. Group members are always logged in, so they never get a guest copy, and their own pages are never stored.
WP Super Cache
- Cartnorm adds
cartnorm_b2b_groupto WP Super Cache’s cookie list (do_action( 'wpsc_add_cookie', 'cartnorm_b2b_group' )), so the cookie becomes part of the cache key. It is removed again on deactivation (wpsc_delete_cookie). - WP Super Cache honours
DONOTCACHEPAGE. - Recommended: keep “Disable caching for visitors who have a cookie set in their browser” or “Disable caching for logged in visitors” on.
FlyingPress
- Cartnorm returns
falsefrom theflying_press_is_cacheablefilter for group pages and definesDONOTCACHEPAGE. - Not verified: FlyingPress is commercial and its source was not available for testing. Please run the self-test after installing.
Other caches (hosting, Varnish, nginx fastcgi_cache, Cloudflare)
Server and CDN caches outside WordPress cannot be configured by a plugin. They usually respect Cache-Control: private, no-store, but some are set up to ignore it.
- Bypass the cache when the request has a cookie
wordpress_logged_in_*,cartnorm_b2b_group,woocommerce_items_in_cartorwp_woocommerce_session_*. - Cloudflare APO / “Cache Everything” rules: add a bypass on the cookies above. APO bypasses
wordpress_logged_in_*by default. - B2B only mode: all pages are sent with
no-store. If your host caches HTML regardless, ask your host to exclude the shop, or the whole site.
Example for nginx fastcgi_cache:
if ($http_cookie ~* "wordpress_logged_in_|cartnorm_b2b_group|woocommerce_items_in_cart|wp_woocommerce_session_") {
set $skip_cache 1;
}
Example for Varnish (VCL, vcl_recv):
if (req.http.Cookie ~ "wordpress_logged_in_|cartnorm_b2b_group") {
return (pass);
}
Self-test
WooCommerce → Settings → B2B → Store mode & caching → Run self-test loads your shop page like an anonymous visitor (no cookies) from your server and checks:
- that the page is not a page rendered for a B2B group (marker
cartnorm-b2b-view), - that it contains no B2B net price markup (the “excl. VAT” suffix of groups or a quantity price table),
- in “B2B only” mode: that no prices are shown and that the response has a
no-storeheader; a redirect to the login page counts as “passed” when the catalog is restricted, - which cache headers it saw (for example
x-litespeed-cache,cf-cache-status,age).
The test is best effort: it cannot see caches that only apply to visitors from outside (for example a CDN in front of your server), and it only checks one page. To test fully: log in as a wholesale customer in one browser, open some product pages, then open the same pages in a private window without logging in and check the prices.
Developer reference
| Hook | Type | Use |
|---|---|---|
cartnorm_b2b_private_page | filter | Whether the current page must not be cached (bool). |
cartnorm_b2b_cache_plugins | filter | Detected cache plugins (id => [ name, detected ]). |
cartnorm_b2b_store_locked | filter | Whether the current visitor is kept out of a “B2B only” shop. |
Cookie name: Cartnorm\B2B\Cache::COOKIE (cartnorm_b2b_group). Value for a group: Cartnorm\B2B\Cache::cookie_value( $slug ).
What was tested
- Headers,
DONOTCACHEPAGE, the cookie (value, update, removal, forging) and the Store API header: automated tests. - WP Rocket, LiteSpeed Cache and WP Super Cache: the filters and actions were checked against the plugins’ source code (GitHub) and tested with stubs. Not tested with the real plugins running.
- W3 Total Cache:
DONOTCACHEPAGEhandling checked in its source code. - FlyingPress: not verified (no source available).
Something unclear or missing? Write to [email protected].