Disclosure: I run SPBros. I sell a dataset and run a hosted scraper, both linked at the end. Everything before that works without them.
If you're building a Shopify app, or sizing up a category before you start, it helps to know what competitors charge and how many reviews the apps on page 1 have. Category pages show ratings and review counts, but the plan details are on each app's own page. Here's how to pull both into pandas.
1. Find stable hooks in the HTML
Open a category page, for example https://apps.shopify.com/categories/marketing-and-conversion-marketing-email-marketing, and inspect an app card. Skip the class names. Each card carries data-* attributes (example card, trimmed):
<div data-controller="app-card"
data-app-card-handle-value="klaviyo-email-marketing"
data-app-card-name-value="Klaviyo: Email Marketing & SMS"
data-app-card-intra-position-value="1">
I keyed on these instead of the class names, which look auto-generated and likely to change.
2. Parse the cards
pip install httpx beautifulsoup4 pandas (Python 3.8+ for the :=), then:
import re, time, httpx
from bs4 import BeautifulSoup
import pandas as pd
UA = {"User-Agent": "Mozilla/5.0 (compatible; my-research-script; you@example.com)"}
def cards(category): # page 1 of the category only
html = httpx.get(f"https://apps.shopify.com/categories/{category}", headers=UA,
follow_redirects=True, timeout=30).text
out, seen = [], set()
for c in BeautifulSoup(html, "html.parser").select('[data-controller="app-card"]'):
handle = c.get("data-app-card-handle-value")
if not handle or handle in seen:
continue # skip repeat placements
seen.add(handle)
lines = c.get_text("\n", strip=True).split("\n")
rating = next((float(t) for t in lines if re.fullmatch(r"\d\.\d", t)), None)
reviews = next((int(m.group(1).replace(",", "")) for t in lines
if (m := re.fullmatch(r"\(([\d,]+)\)", t))), None)
out.append({"category": category, "rank": len(out) + 1, "handle": handle,
"name": c.get("data-app-card-name-value"),
"rating": rating, "reviews": reviews})
return out
Two gotchas:
- Sponsored cards. The App Store can show ads, and you should drop them if you want organic rank. I don't have a reliable marker to share. If one shows up, inspect it and filter on whatever distinguishes it from the organic cards.
- Repeat placements. The same app can appear more than once on a page. Dedupe on the handle before you number anything.
Shopify also reorganizes categories. For example, the old store-design-page-builders URL now redirects to store-design-storefronts. Keep follow_redirects=True and log the final URL you actually scraped.
3. Get the cheapest paid plan from each app page
The full plan list is on the app's own page (https://apps.shopify.com/<handle>). A regex over the page text catches most of them. Spot-check a few apps, though, because anything else on the page that looks like $X/month will match too. Yearly prices are converted to monthly, and free plans are ignored:
PRICE = re.compile(r"\$([\d,]+(?:\.\d+)?)\s*/\s*(month|year)")
def cheapest_paid(handle):
html = httpx.get(f"https://apps.shopify.com/{handle}", headers=UA,
follow_redirects=True, timeout=30).text
text = BeautifulSoup(html, "html.parser").get_text(" ", strip=True)
monthly = [float(p.replace(",", "")) / (12 if per == "year" else 1)
for p, per in PRICE.findall(text)]
paid = [p for p in monthly if p > 0]
return min(paid) if paid else None # None = no paid plan found
Sleep between requests, cache the HTML while you develop, and check robots.txt first (see section 5).
4. Analyze
rows = []
for cat in ["marketing-and-conversion-marketing-email-marketing"]: # add more categories
for app in cards(cat):
app["lowestPaidPlanUsd"] = cheapest_paid(app["handle"])
rows.append(app)
time.sleep(1.5) # be polite
df = pd.DataFrame(rows)
print(df.groupby("category").agg(apps=("handle", "count"),
median_reviews=("reviews", "median"),
median_cheapest_paid=("lowestPaidPlanUsd", "median")))
For comparison, here's a snapshot I collected on Oct 16, 2026 (UTC). It covers page 1 of three categories: 70 organic apps and 197 pricing-plan rows. Your numbers will differ. Listings change, and a page-wide regex can pick up prices that aren't plans.
| Category (page 1, Oct 16) | Apps | Median reviews | Median cheapest paid plan |
|---|---|---|---|
| email marketing | 12 | 1,942.5 | $15 |
| page builders¹ | 26 | 239 | $27 |
| returns & exchanges² | 32 | 209 | $9.99 |
¹ URL now redirects to store-design-storefronts. ² Page mixed support/contact/returns apps; URL now redirects to store-management-support.
A few observations:
- In email marketing, the median review count on page 1 was about 1,940. A new app there competes with a lot of social proof.
- Page builders had the highest median cheapest paid plan of the three ($27).
- 69 of the 70 apps listed a free plan.
It's one snapshot of page 1 in three categories. Treat it as a starting point.
5. Keep it polite
Check robots.txt (Python's urllib.robotparser handles it in a few lines), collect only public business data, skip reviewer names, keep request rates low, and identify yourself in the User-Agent. Read the site's terms before you reuse or redistribute the data.
If you'd rather not run it yourself
-
The Oct 16 data from this post, $12: 70 app records, 197 pricing-plan rows, CSV/JSON, an Excel workbook (category summary, top apps by reviews, apps, plans) and
analyze.pyto re-run the numbers. Shopify App Store Dataset Sample + Analysis Template (Oct 2026) - A hosted scraper for the same pages that accepts category, app or developer URLs, at $2 per 1,000 results: Shopify App Store Intelligence on Apify
Both are mine (SPBros). Questions about the parsing are welcome in the comments.
Top comments (0)