How we leak-test every WooCommerce plugin before it ships
A plugin that hides the price on the product page but leaks through the Store API hides nothing. The surfaces we test before shipping, and four we cannot.
Updated
In this article5 sections
If a plugin promises to hide your WooCommerce prices, there’s a simple test it has to pass: can an anonymous visitor still find the price anywhere? Not just on the product page — but through the API, the structured data, the social preview tags, the feeds, and every other surface WordPress quietly exposes.
The usual recipe is one filter on woocommerce_get_price_html, which covers the rendered price HTML and leaves every other surface exactly as it was. The plugins built on that recipe inherit the gap along with it. At Eren Labs we build the opposite way: we try to break our own plugins first. We call it leak-testing, and it’s the standard every plugin in our catalog has to meet before it ships.
What a “price leak” actually looks like#
A price leak is any place a value you asked to hide is still readable by someone who isn’t logged in. WooCommerce and WordPress render the same product data in a surprising number of formats, and hiding it on one doesn’t hide it on the others.
These are the surfaces on our sweep list. The third column is the one that matters, because they are not equally dangerous. Three of the eight leak on a default store; a fourth leaks the moment a product has variations; three more only leak under a condition you can check in a minute; and one is a credentials problem wearing a display problem’s clothes.
| Surface | Who reads it | When it actually leaks |
|---|---|---|
| Product page and shop loop HTML | Every visitor, in view-source | Always, until something filters it — woocommerce_get_price_html covers this surface and only this surface |
Store API, /wp-json/wc/store/v1/products | The All Products and product-grid blocks, headless front ends | Always. The route is unauthenticated by design and nothing on it passes through the price HTML filter |
JSON-LD offers.price | Search engines, and anyone reading the source | Always on a default store — and a second time over if Rank Math, Yoast, AIOSEO or SEOPress is active, because each re-injects its own application/ld+json block |
wc-ajax variation responses and the inline data-product_variations JSON | The add-to-cart script, on every variable product | Whenever the product is variable. The script writes the price it read out of that JSON into the page after a shopper picks an attribute |
REST wc/v2 and wc/v3 | Integrations holding consumer keys | Only if a key has been exposed. Key holders are meant to see prices, so this is credential hygiene rather than a display setting |
| WPGraphQL / WooGraphQL | Headless and decoupled front ends | Only if that plugin is installed. If it is, price, regularPrice and salePrice resolve through their own code path |
| Open Graph and Twitter meta | Social previews, link unfurlers | Only if your theme or SEO plugin emits a price meta tag. Most do not; some do, and the tag sits in the <head> where nobody looks |
| RSS and Atom feeds | Feed readers, aggregators, scrapers | Only if something renders a price into the feed. The core feeds carry the product description; a feed plugin built for Merchant Center assembles its own XML from the raw price |
One surface that turns up on lists like this does not belong on ours. The WordPress oEmbed response returns a title, an author, a thumbnail and an embed iframe; there is no price field in it to leak.
The Store API is the row worth dwelling on, because it is the one that catches people out after a move to a block theme. The cart and checkout blocks read /wp-json/wc/store/v1/cart; the All Products and product-grid blocks render from /wp-json/wc/store/v1/products. That route has to answer before anyone has logged in, which is exactly why it carries no authentication: an anonymous visitor can request it in a browser tab and read the prices object out of clean JSON.
It is also a separate schema with its own extensibility surface — ExtendSchema and the woocommerce_store_api_* hooks — none of which is a switch for hiding a price. Closing it means rewriting the response after the route handler has run, which is a second, deliberate piece of work that snippet-only approaches never get to. Variable products make the point sharper still, because the same payload publishes a price_range with a minimum and a maximum: we went through the price range on variable products separately, including the view-source and single unauthenticated REST call that settle whether your own store is clean.
A plugin that hides the price on the product page but leaks it through the Store API isn’t hiding anything. It’s giving store owners a false sense of security.
How we test — adversarially, before release#
Instead of trusting that hiding worked, we run an adversarial harness that sweeps every public surface as an anonymous visitor and searches for the exact values we promised to remove. A release only ships when that sweep returns zero leaks.
- Enumerate every surface. The eight rows in the table above: product page and loop HTML, Store API, REST v2/v3, GraphQL, JSON-LD, OG/Twitter tags, feeds, and the
wc-ajaxvariation endpoints. - Search for the real values. Not “did the page change” — but “is this price present anywhere in any response” — and in both of the spellings a store uses for it: the rendered string with its thousands separator,
1,299, and the minor-unit integer the Store API returns for the same amount,129900. Grep for one and not the other and a leaking store reports clean. - Fight back with the ecosystem. We install Rank Math, Yoast, AIOSEO and SEOPress one at a time — each re-injects price schema — and confirm that every block of structured data on the page comes back without a price.
- Test across themes and builders. Storefront, Astra, GeneratePress, Kadence, Flatsome and more, plus Elementor — because a theme can re-render a price its own way.
- Guard against regressions. The same sweep runs in our CI on every WooCommerce release and fails the build the moment a single value leaks again. It is not a harness you have to take on trust, either: the same checks ship in PriceVeil as WP-CLI commands, so you can run them against your own store.
wp priceveil selftestwalks every product and asserts that the price HTML, the structured data, the Store API product response and the collection price range all come back blank, exiting non-zero on the first leak;wp priceveil scanreports exposure per surface as JSON you can diff in your own pipeline.
Why this matters for your store#
If you run a B2B, wholesale, or made-to-order shop, hiding prices behind a quote request is a business requirement, not a nice-to-have. A leak isn’t cosmetic — it can undercut your pricing strategy, expose margins to competitors, or break a contractual “prices on request” arrangement.
Leak-testing is also why we can support modern WooCommerce with confidence: High-Performance Order Storage (HPOS), the block/Store API checkout, and the latest releases are primary test targets, not afterthoughts.
What leak-testing does not cover#
A sweep that returns zero leaks says one specific thing: on this install, on this theme, at this moment, the price is not readable on any surface our code can reach. Four things sit outside that boundary, and no amount of testing on our side closes them. They are worth knowing before you trust any plugin’s clean bill of health, ours included.
- A full-page cache or CDN still holding the old HTML. If the shop page was cached while the price was visible, the edge keeps serving that copy until someone purges it. We cannot purge a cache we do not control — that one is yours, and while you are in there, exclude the Store API and REST routes from HTML and CDN caching entirely.
- Product feeds built by other plugins. A feed plugin writing XML for Google Merchant Center or a Meta product catalog reads the raw price and assembles its own output. It never passes through Woo’s display hooks, so nothing we filter reaches it. Exclude those products in the feed plugin’s own settings, or switch the feed off.
- A theme or page builder printing its own markup. The four SEO plugins above are handled because we went and handled them one at a time. A bespoke template that calls
get_price()and echoes the result, or emits its ownapplication/ld+json, is outside every hook there is. Editing that template is the only thing that fixes it. - An exposed REST key.
wc/v2andwc/v3hand a key holder the whole product object, price included, and that is the API working as designed. If the key has leaked, the problem is credentials rather than display, and the fix is to revoke it.
We test what we can reach. The list above is what we cannot, and a plugin that does not tell you where its coverage stops is selling the same false confidence as one that hides the product page and nothing else.
The bar for every Eren Labs plugin#
Leak-testing started with PriceVeil — Request-a-Quote, Hide-Price and Catalog Mode in one plugin — but it’s the bar we hold every plugin to. When we fix a leak once in our shared core, every plugin in the catalog inherits the fix.
PriceVeil is live. The free plugin — hide prices, catalog mode and quote requests — is on WordPress.org, and PriceVeil Pro turns those requests into priced quotes your customer can accept and pay online. Pro is $69 a year for one site, with a 14-day money-back guarantee.
None of which is an argument for installing a plugin you do not need. Plenty of shops run a classic theme with no headless front end and no public API keys, and for those a handful of filters genuinely does the job. Start there: hide the price until a customer logs in, or go further and run full catalog mode without a plugin. Both write-ups end where this one does — with the surfaces the snippets leave open, and how to check your own store rather than trust what the page looks like.
