DEV Community

Cover image for How to export Shopify App Store reviews to CSV with Python (no API key)
Freshactors
Freshactors

Posted on

How to export Shopify App Store reviews to CSV with Python (no API key)

If you build Shopify apps — or research the market — the reviews are the most valuable data on each listing: the star rating, the actual complaint text, which country the merchant is in, how long they used the app, and whether the developer replied. But there's no official Shopify App Store API, and the review HTML is a moving target. This post shows how to pull reviews for any app — or a whole set of competitors — and export them straight to CSV for analysis, in a few lines of Python. No API key for Shopify, no login, no headless browser.

We'll use the hosted Shopify App Store Scraper actor on Apify, which handles pagination and the brittle parsing for you. If you'd rather not write any code at all, there's a one-click Shopify App Reviews Export example that returns the same data as JSON/CSV straight from the browser.

What we'll build

  1. Export one app's reviews to a CSV.
  2. Pull reviews for several competing apps in a single run.
  3. A quick analysis: rating distribution, developer-reply rate, and the words that show up most in 1–2★ reviews.

Prerequisites

  • Python 3.8+
  • The client and pandas: pip install apify-client pandas
  • A free Apify token (Console → Settings → Integrations), kept out of source control in an env var:
export APIFY_TOKEN="apify_api_xxx"
Enter fullscreen mode Exit fullscreen mode

1. Pull reviews for one app

The review handle is the slug at the end of a listing URL — https://apps.shopify.com/klaviyo-email-marketingklaviyo-email-marketing. Set mode to reviews, pass one or more handles in appHandles, and cap how many reviews per app:

import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])

run_input = {
    "mode": "reviews",
    "appHandles": ["klaviyo-email-marketing"],
    "maxReviewsPerApp": 200,
}

run = client.actor("freshactors/shopify-app-store-scraper").call(run_input=run_input)
reviews = list(client.dataset(run["defaultDatasetId"]).iterate_items())

print(f"pulled {len(reviews)} reviews")
print(reviews[0])
Enter fullscreen mode Exit fullscreen mode

Each review record looks like this:

{
  "handle": "klaviyo-email-marketing",
  "reviewId": "123456789",
  "rating": 5,
  "storeName": "Acme Coffee Co.",
  "country": "United States",
  "usedAppFor": "About 2 years using the app",
  "date": "June 12, 2026",
  "body": "Great app — support helped us migrate our flows in a day.",
  "developerReply": "Thanks so much for the kind words! ...",
  "developerReplyDate": "June 13, 2026"
}
Enter fullscreen mode Exit fullscreen mode

One thing worth knowing up front: some reviews are rating-only (a star with no text), so body is legitimately empty on those — that's real data, not a scrape failure. The actor also retries transparently when Shopify throttles a page, so you get the complete set instead of a silent partial one.

2. Export to CSV

pandas makes this a one-liner (the built-in csv module works too if you'd rather skip the dependency):

import pandas as pd

df = pd.DataFrame(reviews)
cols = ["date", "rating", "country", "usedAppFor", "storeName",
        "body", "developerReply", "developerReplyDate"]
df[cols].to_csv("klaviyo_reviews.csv", index=False)
Enter fullscreen mode Exit fullscreen mode

Open that in Excel or Google Sheets and every review is a row.

3. Several competitors in one run

reviews mode takes a list, so pass a whole competitive set at once — the actor fetches each app and tags every record with its handle so you can group them afterward:

run_input = {
    "mode": "reviews",
    "appHandles": [
        "klaviyo-email-marketing",
        "privy",
        "omnisend",
    ],
    "maxReviewsPerApp": 300,
}

run = client.actor("freshactors/shopify-app-store-scraper").call(run_input=run_input)
df = pd.DataFrame(client.dataset(run["defaultDatasetId"]).iterate_items())
df.to_csv("email_apps_reviews.csv", index=False)
Enter fullscreen mode Exit fullscreen mode

4. A five-minute analysis

Now that it's a DataFrame, a couple of aggregations say a lot about how each competitor is doing:

# average rating + review volume per app
print(df.groupby("handle")["rating"].agg(["mean", "count"]))

print(df.groupby("handle")["rating"].agg(["mean", "count"]))

# developer responsiveness: share of reviews they replied to
df["has_reply"] = df["developerReply"].notna()
print(df.groupby("handle")["has_reply"].mean().round(2))

# what unhappy merchants complain about: top words in 1–2★ reviews
from collections import Counter
import re

low = df[df["rating"] <= 2]["body"].dropna()
words = re.findall(r"[a-z]{4,}", " ".join(low).lower())
stop = {"this", "that", "with", "have", "they", "your", "would", "were",
        "when", "from", "them", "just", "been", "very", "will", "which"}
common = Counter(w for w in words if w not in stop).most_common(15)
print(common)
Enter fullscreen mode Exit fullscreen mode

That last snippet is a cheap, surprisingly effective product-gap map: when words like support, billing, sync, or charges bubble to the top of a competitor's 1★ reviews, you're looking at exactly where merchants are churning.

Pricing & why "hosted" matters

The actor is Pay-Per-Event: $0.0001 per review with no start or subscription fee, so a 300-review pull runs about $0.03. It's pure HTTP/JSON — no headless browser to babysit — and it's monitored every day with a public "last verified working" badge, because the entire point of a hosted scraper is that it keeps working when the site's markup shifts underneath it.

Try it

Have a Shopify-reviews question or hit an edge case? Drop it in the comments — happy to help.

Top comments (0)