DEV Community

Devil Scrapes
Devil Scrapes

Posted on

Shopify App Store Scraper: pricing tiers are text, not a number

Quick answer

Shopify's App Store doesn't expose pricing as a number, doesn't expose an install-count signal anywhere, and its category pages return exactly 40 apps with no working pagination. The Shopify App Store Scraper reads app-card markup plus each listing's structured application/ld+json block and returns 13 typed fields per app — either for specific handles you name or for the top 40 apps in a category — at $0.02 per run-start plus $0.002 per row ($2.02 for 1,000 apps).

Pricing tiers are text, not a number, and that's the correct answer 💰

If you're rolling your own Shopify App Store scraper, the first trap is coercing price into a float so you can sort apps cheapest-to-priciest. Don't. Shopify's #adp-pricing section mixes flat monthly fees ("$29.99/month"), free-to-install apps, usage-based charges, and trial-qualified phrasing in the same section, sometimes on the same app. A single numeric field can't hold that without losing information — a $0 parsed from "Free plan" looks identical to a $0 parsed from a trial that becomes $49/month after 14 days.

Our PricingPlan model keeps price as raw text on purpose, alongside a separate additional_charges string for the "usage fees may apply" fine print under a lot of tiers. pricing_plans is an ordered list — most apps ship 2-4 tiers — and it's an empty list, never a missing key, when a listing has no pricing section at all. If you want a sortable number, that's a downstream decision you make with full information, not one we make for you by throwing data away.

install_count is always null, and that's not a bug 🚫

Shopify does not publish a merchant-count, install-count, or "used by N stores" signal anywhere on the App Store — not on the card, not in the LD+JSON block. Some competing scrapers infer a proxy number from review count or category rank and present it as an install estimate. We don't. install_count ships null on every row, always, because inventing a number that looks precise but isn't is worse than admitting Shopify doesn't tell you.

What Shopify does expose reliably is rating_value and review_count, both pulled from the LD+JSON aggregateRating block on the detail page rather than scraped from rendered star icons — more stable across Shopify's front-end changes than HTML text. Both fields go null together, under the same condition: an app with zero reviews has no aggregateRating object to read from, so there's nothing to parse rather than a misleading 0.0.

Category discovery caps at 40, and combined modes dedupe by handle 🔢

The two input modes behave differently. appHandles is the primary, most reliable mode — give it bare handles like klaviyo-email-marketing or full apps.shopify.com URLs (either normalizes to the handle) and you get exactly those apps' detail pages. categorySlugs is discovery mode: point it at a category like marketing-and-conversion and you get back whatever app cards that category page renders.

The part that bites people building this cold: Shopify's category pages return exactly 40 app cards with no working pagination parameter. That's a platform ceiling, not our maxItems default. categorySlugs is a top-40 snapshot, not exhaustive enumeration — there's no documented way to get more.

Combine both modes in one run and results dedupe by handle, with appHandles processed first. If an app appears in both your handle list and your category discovery, the row keeps discovered_via: "app_handles"; the category-card pass only fills name / icon_url gaps when a detail-page fetch comes back empty, it never overwrites data already there.

What we handle for you 🛡️

  • We rotate browser fingerprintscurl-cffi impersonation across Chrome and Firefox TLS profiles, rotating between attempts, so requests present as a browser rather than a Python client.
  • We retry with exponential backoff on 408 / 429 / 5xx, honoring Retry-After, rotating impersonation profiles on each retry.
  • A broken detail page never fails the run. A missing pricing section or malformed listing degrades that one row to pricing_plans: [] and null fields instead of aborting the whole batch.
  • We prefer structured data over brittle HTML. Name, developer, icon, rating, and review count come from each detail page's application/ld+json block first — it survives front-end redesigns that would break a text-scraping approach.
  • We keep the dataset clean — Pydantic-validated rows, ISO-8601 timestamps, stable handles, JSON / CSV / Excel export straight from the Apify Console.
  • You pay only for results that land. No data, no charge, beyond the small run-start warm-up fee.

Full output schema 📦

Thirteen fields per app:

Field Type Notes
handle string App's canonical URL handle
name string App name, from the detail page's LD+JSON block
developer string Developer/company name, from LD+JSON brand
url string Canonical https://apps.shopify.com/{handle} URL
icon_url string | null App icon URL; falls back to the category-card icon if the detail fetch fails
category string | null Category-link text next to the "Launched" row; null when not discoverable
pricing_plans array Ordered list of {name, price, additional_charges} tiers; empty list when none found
rating_value float | null Aggregate rating (0-5) from LD+JSON; null if the app has no reviews
review_count int \ null
launched_date string | null ISO-8601 launch date, parsed from the "Launched" row's text
works_with array Integration/works-with tags; empty list when none found
install_count int | null Best-effort only — no such signal is exposed by Shopify; ships null in v1
discovered_via string app_handles or category_slugs — which input mode produced this row
scraped_at string ISO-8601 UTC timestamp when the row was recorded

Who this is for

Competitive pricing research — pull every tier for the apps in your category and see the real spread of flat, trial, and usage-based pricing before you set your own.

DTC agency app-stack vetting — check rating_value, review_count, and works_with for the apps you're about to recommend to a client.

Market-intel snapshots — pull a category's top 40 apps (categorySlugs) on a schedule to watch pricing and ratings shift; each run is a point-in-time snapshot, so delta tracking is on your scheduling.

Integration audits — filter on works_with to confirm an app actually supports Shopify Flow, POS, or Checkout before you commit engineering time.

Frequently asked questions

Is price a parsed number?
No. It ships as raw text — "$29.99/month", "Free to install" — because pricing tiers mix flat, trial-qualified, and usage-based phrasing a single numeric field can't hold without losing information.

Why is install_count always null?
Shopify doesn't expose a merchant-count or install-count signal anywhere on the App Store. We ship null rather than infer a proxy number.

Can I get more than 40 apps from a category?
Not through this surface. Shopify's category pages cap at 40 app cards with no working pagination parameter — a platform limit, not an Actor limit.

What happens if an app appears in both appHandles and categorySlugs?
Results dedupe by handle, appHandles processed first. The row keeps discovered_via: "app_handles"; category-card data only fills gaps if the detail-page fetch failed.

What does 1,000 apps cost?
$2.02 — 1,000 × $0.002, plus the one-off $0.02 run-start charge.

Try it

Live on the Apify Store: Shopify App Store Scraper.

Point it at specific app handles, a handful of category slugs, or both — get back typed pricing, ratings, and integration rows without hand-parsing LD+JSON yourself. Pay-per-event, no subscription.


Built by Devil Scrapes — we build scrapers for the targets that fight back.

Top comments (0)