# Shopify App Store & Theme Store Scraper - Apps and Themes

Scrape Shopify App Store and Theme Store listings by category: name, developer, rating, review count, pricing and the Built for Shopify badge. Category and sitemap discovery.

Canonical product page: https://mayd-it.com/data-tools/actors/shopify-app-store-scraper/
Public Actor: https://apify.com/maydit/shopify-app-store-scraper

## Example input

```json
{
  "mode": "apps",
  "categories": [
    "store-design-site-optimization-seo"
  ],
  "includeDetails": false,
  "maxItems": 10,
  "maxRunSeconds": 240,
  "onlyNewSinceLastRun": false
}
```

## Full guide

![Map the apps and themes on offer — illustrative workflow](https://api.apify.com/v2/key-value-stores/YGxD9JRpduom8Q5Zd/records/shopify-app-store-scraper-4c8f8970de2d.png?signature=Vzdq3ipRWzJavTppFEQj)

## Map the apps and themes on offer

For marketplace research: collect public Shopify App Store and Theme Store listings by category or collection, with names, source links and published pricing or review signals. Optional app detail pages add developer information.

[Open this Actor in Console](https://console.apify.com/actors/xQBN7kmaZo2mzBD7E/input) · [First run](#try-this-actor) · [Results](#result-preview) · [Cost](#usage-and-pricing) · [Full guide](#full-guide)

## Try this Actor

- **First run:** Use the existing **apps** Console sample and its SEO category. Review the 60-listing cap; leave app details off for the initial listing view unless you need the developer field.
- **Read the results:** Open the default Dataset for listing rows and **SUMMARY** for collection status. The shared table contains both app and theme fields, so use the record type when comparing results.
- **Keep in mind:** App star ratings and theme positive-review percentages are different measures. Published pricing text is not a complete normalized subscription cost, and fields vary between apps and themes.

## Result preview

The dataset view highlights these columns:

- **Listing type** · `type`
- **App or theme name** · `name`
- **Published developer** · `developer`
- **Developer URL** · `developerUrl`
- **Store handle** · `handle`
- **Store listing URL** · `url`

Use the original source links and the field definitions below when interpreting the results. CSV works well for flat columns; JSON preserves nested values.

## Usage and pricing

Base result rate on the FREE tier: **$2 per 1,000 results**. The [Pricing tab](https://apify.com/maydit/shopify-app-store-scraper/pricing) shows your plan's result rate and any Actor Start charge. Set a small result limit for the first run; the requested limit is not a guarantee of that many results.

---

## Full guide

Export Shopify App Store listings and Shopify Theme Store listings as clean, structured rows. Pick a category (or a theme collection), and results include published **rating, review count, pricing text, tagline and applicable badge fields**. Developer details depend on the selected enrichment options and successful source retrieval. Export results as CSV, JSON or Excel. Use it for Shopify app market research, competitor tracking, app store SEO and category monitoring, partner prospecting, or building a dataset of the Shopify ecosystem.

Apps and themes share one schema, so `type: "app"` and `type: "theme"` rows sit in the same dataset and the same spreadsheet columns.

### What you get

One row per app or theme.

| Field | Type | Description |
| --- | --- | --- |
| `type` | string | `app` or `theme`. |
| `name` | string | Listing name as shown in the store. |
| `developer` | string \| null | App developer / theme designer. Apps: only when **Also open each app's page to get the developer** is enabled (see Input), and can still be `null` on a row whose detail page was throttled. Themes: filled whenever the theme's detail page loads. |
| `developerUrl` | string \| null | The developer's page on the store (`/partners/...` for apps, `/designers/...` for themes). Same rule as `developer`. |
| `handle` | string | URL slug, e.g. `judgeme`, `prestige`. Stable primary key. |
| `url` | string | Canonical listing URL, with Shopify's tracking parameters removed. |
| `tagline` | string \| null | The one-line pitch under the listing name. |
| `category` | string | The category or collection slug this row was collected from. |
| `rating` | number \| null | Star rating out of 5. **Apps only** - the Theme Store does not publish stars (see `positivePercent`). |
| `reviewCount` | number \| null | Number of reviews. For a theme with several presets this is Shopify's "reviews for all presets" total. |
| `positivePercent` | number \| null | **Themes only** - Shopify's "% positive" merchant score, 0-100. Always `null` for apps. |
| `pricingSummary` | string \| null | The pricing line exactly as the store shows it: `Free`, `Free plan available`, `Free to install`, `Free trial available`, `$39/month`, or `$300 one-time` for a paid theme. |
| `priceFrom` | number \| null | The number parsed out of `pricingSummary`. `0` for free / freemium, the monthly or one-time amount when the store shows one, `null` when the card only says "Free trial available" (the store does not print the price there). |
| `builtForShopify` | boolean | Whether the listing carries Shopify's **Built for Shopify** badge. Always `false` for themes - that badge is an App Store programme. |
| `iconUrl` | string \| null | App icon, or the theme's preview image. |
| `scrapedAt` | string | ISO timestamp of the run. |

A `SUMMARY` record is written to the run's key-value store with the sources used, how many rows were written, how many requests were throttled, and whether the run stopped on its time budget.

### Input

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | select | `apps` | `apps` scrapes apps.shopify.com, `themes` scrapes themes.shopify.com. Anything else is treated as `apps`. |
| `categories` | string list | `["store-design-site-optimization-seo"]` | App Store category slugs, or Theme Store collection slugs in themes mode. Empty falls back to one fast sample category. Full category URLs work too. |
| `maxItems` | integer | `60` | Total rows across all selected categories. |
| `includeDetails` | boolean | `false` | Open each app's own page to read the developer name. Costs one extra request per app. Ignored in themes mode, where the detail page is always loaded. |
| `onlyNewSinceLastRun` | boolean | `false` | Return only listings this Actor has not returned before for the same mode and category set. |
| `maxRunSeconds` | integer | `240` | Wall-clock budget. The crawl stops cleanly when it runs out and keeps everything collected so far. |

**Where do category slugs come from?** The App Store's own sitemap lists all of them: <https://apps.shopify.com/sitemap_categories_en.xml>. Examples: `store-design-site-optimization-seo`, `marketing-and-conversion-marketing-email-marketing`, `orders-and-shipping-shipping-solutions-shipping`, `store-management-operations-workflow-automation`.
Third-level (leaf) categories are the paginated ones and can return hundreds of apps. A top-level or mid-level slug like `store-design` has no browse grid on Shopify's side; the Actor detects that and reads the curated category page instead, which is about 30 apps on one page.

**Theme collections:** `free-themes`, `trending-themes`, `new-themes`, `horizon-themes`, `large-inventory`, `small-inventory` (see <https://themes.shopify.com/sitemap_collection_en.xml>), or the keyword `all` for the main `/themes` page.

### Example output

An app row (`mode: apps`, `includeDetails: true`):

```json
{
  "type": "app",
  "name": "Klaviyo: Email Marketing & SMS",
  "developer": "Klaviyo",
  "developerUrl": "https://apps.shopify.com/partners/klaviyo",
  "handle": "klaviyo-email-marketing",
  "url": "https://apps.shopify.com/klaviyo-email-marketing",
  "tagline": "Email marketing, SMS, & WhatsApp that turn messages into sales",
  "category": "marketing-and-conversion-marketing-email-marketing",
  "rating": 4.7,
  "reviewCount": 3319,
  "positivePercent": null,
  "pricingSummary": "Free to install",
  "priceFrom": 0,
  "builtForShopify": false,
  "iconUrl": "https://cdn.shopify.com/app-store/listing_images/5edd9000b933a8fa88c152d1e498531f/icon/CP6B2OOv3PYCEAE=.png",
  "scrapedAt": "2026-09-24T01:02:45.424Z"
}
```

A theme row (`mode: themes`, `categories: ["trending-themes"]`):

```json
{
  "type": "theme",
  "name": "Vivid",
  "developer": "Slash Themes",
  "developerUrl": "https://themes.shopify.com/designers/slashthemes",
  "handle": "vivid",
  "url": "https://themes.shopify.com/themes/vivid/presets/vivid",
  "tagline": "Exceptional design and powerful features to elevate your brand",
  "category": "trending-themes",
  "rating": null,
  "reviewCount": 90,
  "positivePercent": 96,
  "pricingSummary": "$300 one-time",
  "priceFrom": 300,
  "builtForShopify": false,
  "iconUrl": "https://cdn.shopify.com/theme-store/w0pa1h0bb3opczjgmmkf8wizcavv.jpg?width=600",
  "scrapedAt": "2026-09-24T01:05:38.866Z"
}
```

### FAQ

**Does this scrape App Store search results?**
No. `apps.shopify.com/robots.txt` contains `Disallow: *q=*` for all user agents, so search-result URLs are off limits and this Actor never requests one - it refuses at the request layer, not just by convention. Discovery is done through category pages, theme collection pages and Shopify's published sitemaps. If you need a keyword view, pull a category and filter the rows yourself.

**Why is `developer` empty on my app rows?**
Because `includeDetails` was off. A category card contains everything except the developer name, so filling that column costs one extra request per app. apps.shopify.com throttles above roughly two requests a second, which is why it is opt-in: 60 apps take about 2 seconds without it and about 90-100 seconds with it (measured 2026-09-23: 95s for 60 apps, 61 requests, 18 of them throttled and retried). Turn it on and raise `maxRunSeconds` for large jobs. When Shopify throttles hard a few rows can still come back with `developer: null` - the Actor keeps the listing row rather than failing the run. Theme rows include the developer whenever the theme's detail page loads, because a theme card carries almost nothing and the detail page has to be read anyway.

**Why is `rating` null for themes?**
The Shopify Theme Store does not publish a 5-star score. It publishes a "% positive" merchant rating and a review count, so those land in `positivePercent` and `reviewCount`, and `rating` stays null rather than being invented from the percentage.

**How does `onlyNewSinceLastRun` work?**
The Actor remembers the handles it has already returned for that mode and category set in a named key-value store. On the next run it looks at the same window - the top `maxItems` listings of each selected category - and returns only the handles it has not returned before. A run where nothing changed finishes successfully with an empty dataset; that is the "no new apps" answer, not a failure. The first run always returns everything.

Category scopes longer than 60 characters now use a full-scope hashed history key. Their first run after this update establishes a fresh baseline and may return previously seen listings once; later runs suppress them normally. Existing truncated-key records are preserved. Shorter scopes retain their existing history keys and behavior. `SUMMARY.historyKeyVersion` identifies the key format used (1 or 2).

**Do you return developer emails or phone numbers?**
No. This Actor collects listing data only. It does not scrape contact details, does not crawl partner contact pages, and does not enrich rows with personal data.

**What happens if the run hits its time limit?**
It stops cleanly, keeps every row already written, logs how far it got, and records `stoppedOnTime: true` in `SUMMARY`. Runs are not lost; re-run with a bigger `maxRunSeconds` or a smaller `maxItems`.

### Data source, access and limits

- **Source:** the public HTML of `apps.shopify.com` and `themes.shopify.com` (English pages), plus Shopify's published sitemaps for the category and collection lists. No API key, no Shopify account, no login, no Partner access required.
- **robots.txt is honored.** No URL with a `q=` parameter is ever requested (App Store search), no query string is ever put on `/themes` (Theme Store filter URLs), and Shopify's `surface_*` tracking parameters are stripped from the URLs it does request. These checks are enforced in code before each request.
- **Promoted placements are excluded.** Shopify mixes up to four paid placements into a category page; they are duplicates of organic listings and are dropped, so a page yields its organic listings only.
- **Rate limits.** Shopify starts returning HTTP 429 above roughly two requests a second, so requests are paced and retried. That sets the speed: 60 listing rows in about 2 seconds (3 requests), or the same 60 rows with developer lookups in about 95 seconds (61 requests, 18 of which were throttled and retried) - a bit over one app per second once detail pages are on. Timings were measured from a residential IP; Apify's datacenter IPs may be throttled harder. Persistent throttling makes the Actor finish with the listing data rather than fail.
- **Coverage.** Leaf categories paginate at 20 organic apps per page, and this Actor reads at most 40 pages per category - about 800 apps from any single category. To go past that, or to reach the 5000-row `maxItems` ceiling, pass several categories. Parent categories have no browse grid and return the ~25-30 apps Shopify curates on that one page. The Theme Store main page and each collection are single pages of roughly 20-25 themes.
- **Structure changes.** This Actor parses HTML. If Shopify changes its markup it fails loudly with an error naming the page and the marker it looked for, instead of writing empty or garbage rows.
- **Billing.** Runs are billed per result on the Apify platform, on top of your Apify plan's usage. Nothing here is free to run.
- Not affiliated with, endorsed by, or sponsored by Shopify Inc. "Shopify", "Shopify App Store" and "Built for Shopify" are trademarks of Shopify Inc., used here only to describe the public pages this Actor reads.


#### Saved rows and run charge limits

The Actor writes results one at a time and stops when Apify reports the run charge limit. `SUMMARY.stoppedOnChargeLimit` distinguishes that stop from an empty source. Saved-row counts include only rows accepted by the dataset. For change detection, prior seen IDs are retained and newly discovered IDs enter the baseline only after their row is saved; rows excluded by a result or charge cap remain eligible for a later run. Avoid concurrent monitor runs sharing the same named baseline store.

## Help with your run

Open the **Issues** tab on this Actor with the run URL, the input field that caused trouble and what you expected to receive. Remove API keys and private information before posting.

[Explore more Mayd-It Actors](https://apify.com/maydit)

## Understand your run

Open **Run report** in the run’s Output tab for saved-record counts, reported source issues and limits, and suggested next steps. Use **CSV export** to take the saved results into a spreadsheet. The existing JSON results and detailed machine-readable summary remain available.

The report reflects this run’s recorded coverage. A successful run or a saved result does not establish that every possible source record was collected.


### Separate comparison history for recurring workflows

Set `stateStoreName` to a unique name such as `my-shopify-app-watch` when a QA check or independent schedule must keep its own history. Reuse the same name for that workflow. Omit this optional field to preserve the existing `shopify-store-intel-state` behavior and its saved comparisons. The new name starts a new baseline; it does not copy or erase an older baseline. Avoid overlapping runs that write to the same history.
