MegalaniaBlog
  1. Blog
  2. How to scrape
  3. WooCommerce

How to scrape any WooCommerce store

Get name, price, stock and SKU from WooCommerce product pages. Try the free Store API first, then one request in cURL or Python, with real page costs.

Jordan HayesDeveloper Relations
6 min read
How to scrape
A shelf price tag flipped in mid-air. The front shows a price, the back shows the same price as JSON in minor units.

The short answer: try the store’s free Store API first, at /wp-json/wc/store/v1/products. If it is off or blocked, send one request with five extract rules for WooCommerce’s default product page: h1.product_title, p.price, p.stock, the add-to-cart button and .sku. On our test run those rules filled name, price, stock and SKU on 32 of 33 pages.

What a product page gives you

WooCommerce runs inside WordPress, so stores look very different. But most themes keep WooCommerce’s own product template, and its class names:

  • Name: h1.product_title.
  • Price: p.price. During a sale it holds the old price in <del> and the new one in <ins>.
  • Stock: p.stock when the store shows a stock line, else the add-to-cart button: no button usually means you cannot buy it now.
  • SKU: .sku_wrapper .sku, when the store fills it in.

The free way: the Store API

Many WooCommerce stores answer a public JSON address with no key. Try it before you pay anyone, us included:

cURL
curl -s "https://scrapeme.live/wp-json/wc/store/v1/products?per_page=1"

On 2 Oct 2026 it answered a JSON list on all three stores we measured. Each product has name, sku, is_in_stock and a prices object. Prices are strings in minor units: "price": "2400" with "currency_minor_unit": 2 means 24.00. Divide by 10 to the power of currency_minor_unit. regular_price and sale_price tell you if it is on sale.

It is not enough when:

  • The store turned the API off, or a security plugin blocks it.
  • It answers your script with a 403, a bot check or 429 after a few calls.
  • You need what the shopper sees on the page.

Check a store first

Before a big run, ask about the host. It is free. Over MCP, your agent calls check_site:

MCP check_site
{"name": "check_site", "arguments": {"host": "barefootbuttons.com"}}

From cURL, the same answer is two calls: /v1/tier for the price, /v1/stats/domain for how our routes have done there lately.

cURL
curl -s -H "Authorization: Bearer YOUR_KEY" \
  "https://get.megalania.io/v1/tier?host=barefootbuttons.com"
curl -s -H "Authorization: Bearer YOUR_KEY" \
  "https://get.megalania.io/v1/stats/domain?host=barefootbuttons.com&days=30"

On 2 Oct 2026 all three stores answered "tier": null: not priced yet, so a page pays the price of the route that delivers it, shown in quotes. Check a site first explains every field.

First request (cURL)

The body is the exact one we ran. Send it with a heredoc, so the quotes need no shell escaping:

cURL
curl -sS https://get.megalania.io/v1 \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -D headers.txt \
  --data @- <<'EOF'
{
  "url": "https://barefootbuttons.com/product/v2-standard-light-blue-footswitch-topper/",
  "response_types": "json",
  "extract": "name:h1.product_title|price:p.price ins .amount, p.price > .amount|regular_price:p.price del .amount|stock:p.stock|add_to_cart:form.cart button.single_add_to_cart_button|sku:.sku_wrapper .sku",
  "max_credits": "64"
}
EOF

One rule is name:selector. The price rule has two selectors: the sale price in <ins> when there is one, else the plain price. regular_price is the struck-through old price, so it is null when nothing is on sale. max_credits is the most this page may cost; we skip any route above it.

Get a free beta key76,000 credits for 30 days. Sign in with Google or GitHub.Apply for the beta

Python: price, stock and SKU

woocommerce.py
# pip install requests
# Run: MEG_API_KEY=YOUR_KEY python woocommerce.py 'https://scrapeme.live/shop/Venusaur/'
import os
import sys
import uuid

import requests

RULES = "|".join([
    "name:h1.product_title",
    "price:p.price ins .amount, p.price > .amount",
    "regular_price:p.price del .amount",
    "stock:p.stock",
    "add_to_cart:form.cart button.single_add_to_cart_button",
    "sku:.sku_wrapper .sku",
])

resp = requests.post(
    "https://get.megalania.io/v1",
    headers={
        "Authorization": f"Bearer {os.environ['MEG_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={"url": sys.argv[1], "response_types": "json", "extract": RULES, "max_credits": "64"},
    timeout=90,
)
if resp.status_code != 200:
    err = resp.json()["error"]
    sys.exit(f"{resp.status_code} {err['code']}: {err['message']}")

item = resp.json()
stock = item["stock"] or ("In stock" if item["add_to_cart"] else "Out of stock")
print(item["name"], "|", item["price"], "|", item["regular_price"], "|", stock, "|", item["sku"])
print("credits:", resp.headers["X-Credits-Charged"], "route:", resp.headers["X-Route"])

A rule that matches nothing comes back as null, so your code sees it instead of crashing. A block is HTTP 422 with error.code blocked and X-Billing: blocked, and it costs 0 credits, so the status check catches it. X-Hops on that answer lists every route we tried.

Sample output

Four product pages through the script above. The products are made up; the shape is real.

name price regular_price stock sku
Walnut cutting board $48.00 null 12 in stock WCB-01
Brass bottle opener $14.00 $18.00 In stock BBO-2
Wool throw, grey $89.00 null Out of stock WT-GRY
Gift card $25.00 null In stock null

The price comes back as the page shows it, with the currency sign. Strip it before you store a number.

What we found: the rules filled all four fields (name, price, stock, SKU) on 10 of 10 pages on barefootbuttons.com, 12 of 12 on scrapeme.live, and 10 of 11 on porterandyork.com, where one page missed one field. No page in our sample was on sale, so regular_price was null on all 33.

What it costs

We fetch on the cheapest route first: plain HTTP on a datacenter exit (h:dc). If the store blocks it, we climb to a residential exit (h:res). A sample answer’s headers:

http
x-credits-charged: 16
x-engine: http
x-hops: h:res:firefox=3506
x-identity: firefox
x-route: http/residential
x-target-status: 200

Measured on our dev cluster with the datacenter routes off, so this climb starts at h:res.

A page like this costs about 2-3 credits on h:dc (47-108 KiB, our measured median-p90), worked out from our price table on 2 Oct 2026, not from a test bill; run check_site (free) to see the price for your site.

A page like this was billed 16-24 credits on h:res (47-108 KiB, our measured median-p90) in our test on 2 Oct 2026; run check_site (free) to see the price for your site.

How it adds up: 1 for the page and 1 for JSON output, plus one more step for each started 128 KiB above the 64 KiB included (1 on h:dc, 8 on h:res). On h:res the page is 15 on a host we have not priced yet.

Store Size (median-p90) h:dc h:res
scrapeme.live 22 KiB 2 16
barefootbuttons.com 47-48 KiB 2 16
porterandyork.com 108-109 KiB 3 24

So 1,000 product pages cost 2,000 to 3,000 credits on h:dc, or 16,000 to 24,000 on h:res. The size is the bytes on the wire, which is what the price uses.

How we measured: 33 product pages on barefootbuttons.com, porterandyork.com and scrapeme.live (a practice store built for scraping), 2 Oct 2026, 23:16 to 23:22 UTC, with the request body above. The three stores we planned first had left WooCommerce, so check the platform before you write rules: the Store API answering is a good sign.

When it fails, and the store’s terms

  • Blocks we detect are free, and so are our errors. Every error has a code and a fix in Error codes.
  • A custom theme may rename the classes. Open the page source once and adjust the rule for that store; a miss is null, never a crash.
  • A page that needs a real browser costs from 15 credits on Browser datacenter. We did not need it on these stores.

This is not legal advice. Fetch public pages only, and do not log in. Read each store’s terms and robots.txt, and follow them. Do not collect personal data, such as reviewer names. Keep your rate low: prices rarely change every minute.

To check these fields every day, send the URLs as one batch: Batches.

Jordan HayesDeveloper RelationsPublished 18 Oct 2026 · measured on our dev cluster, 2 Oct 2026, 33 product pages on 3 stores

Keep reading