All posts
GuideJul 8, 2026 · 8 min

Google Play Reviews in BigQuery: The Pipeline, the Schema, and the Shortcut

No native connector pipes Google Play reviews into BigQuery. Here's the GCS-export path, a unified schema, scheduling, and when to skip the ETL.

RA

The Argus Team

Reply Argus

There's no "Export to BigQuery" button for app reviews. Neither Apple nor Google ships a native connector that drops your review stream into a dataset, so getting Google Play reviews into BigQuery means wiring the plumbing yourself: a source, a transform, a load job, and a scheduler that keeps it fresh. Google at least meets you halfway with an automatic Cloud Storage export. Apple makes you build the whole path.

This is the working version of that pipeline. The cheap route for Play, the API route for Apple, a schema that survives both stores, how to schedule it without piling up duplicate rows, and — if the thing consuming this data is an agent rather than a dashboard — where you can skip most of it entirely.

What's the fastest path for Google Play reviews into BigQuery?

Play Console already exports report CSVs to a Google Cloud Storage bucket on your behalf, and reviews are one of those reports. You'll find the bucket URI under Download reports → Reviews in the console (it looks like `gs://pubsite_prod_rev_<developer-id>/reviews/`). That means the cheapest Play pipeline is just GCS → BigQuery, no API code at all: copy the monthly review CSVs and load them.

One catch that eats an afternoon if you miss it: those Play Console CSVs land UTF-16 encoded, so a loader expecting UTF-8 will produce garbled text or fail outright. Re-encode before loading.

bash
# 1. Pull the review CSVs Play already exported to your GCS bucket
#    (bucket URI is shown in Play Console -> Download reports -> Reviews)
gsutil -m cp -r gs://pubsite_prod_rev_<DEVELOPER_ID>/reviews/ ./reviews/

# 2. Play Console report CSVs are UTF-16 -- re-encode or BigQuery chokes
iconv -f UTF-16 -t UTF-8 \
  reviews/reviews_com.example.app_202607.csv > play_202607.csv

# 3. Load into a raw table (let BQ infer types on first pass)
bq load --source_format=CSV --skip_leading_rows=1 --autodetect \
  reviews.play_raw ./play_202607.csv
The no-API path. Play does the export; you copy, re-encode, and load. The monthly CSV is complete but batchy, so it lags real time by design.

The CSV export is complete history but coarse — files are aggregated per month. If you want reviews within minutes of them landing, pull the `reviews.list` method of the Play Developer API instead and stream the JSON in. The trade-off is that this endpoint only surfaces roughly the last seven days, so it's a freshness feed, not a history feed. The [Google Play reviews API reference](/blog/google-play-reviews-api-reference) maps every field, and [scraping Google Play reviews in Python](/blog/scrape-google-play-reviews-python) covers the unofficial route when neither official window fits. Most complete setups run both: monthly CSVs for the backfill, the API for the live tail.

And the App Store side?

Apple has no GCS export. You call the App Store Connect API's `customerReviews` endpoint, which returns the full paginated history for an app — rating, title, body, reviewer nickname, territory, and created date. You then flatten that JSON into newline-delimited JSON and load it. Auth is its own ritual: a short-lived ES256 JWT you sign yourself with a `.p8` key, expiring within 20 minutes, so a long-running job mints fresh tokens on a schedule.

http
# Apple: paginate the historical feed
GET https://api.appstoreconnect.apple.com/v1/apps/{APP_ID}/customerReviews
    ?sort=-createdDate&limit=200
Authorization: Bearer <ES256 JWT, signed with .p8, expires <= 20m>

# Flatten the response to NDJSON (one review per line), then:
bq load --source_format=NEWLINE_DELIMITED_JSON \
  reviews.appstore_raw ./appstore.ndjson ./appstore_schema.json
No native export for Apple. You own the pull, the JWT refresh, and the flatten-to-NDJSON step before the load.

A schema that survives both stores

The two feeds disagree on field names, so decide your canonical shape once and map both sources onto it. Partition by review date so time-range scans stay cheap, cluster by store and rating because that's how you'll filter, and make `review_id` a store-prefixed dedup key — you'll re-pull overlapping windows constantly and need idempotency.

sql
CREATE TABLE IF NOT EXISTS reviews.unified (
  review_id    STRING NOT NULL,   -- e.g. 'play:xxxx' | 'appstore:xxxx'
  store        STRING NOT NULL,   -- 'play' | 'app_store'
  app_id       STRING NOT NULL,
  rating       INT64,
  title        STRING,
  body         STRING,
  language     STRING,
  territory    STRING,
  author       STRING,
  created_at   TIMESTAMP,
  reply_body   STRING,
  reply_at     TIMESTAMP,
  ingested_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP()
)
PARTITION BY DATE(created_at)
CLUSTER BY store, rating;
One table, both stores. review_id is your MERGE key so re-pulls update rows instead of duplicating them.

Scheduling it so it stays fresh (without duplicate rows)

None of this is one-and-done. Reviews arrive hourly, users edit reviews after posting, and both feeds overlap on every pull. The loop that keeps `reviews.unified` correct looks like this.

  1. 1

    Step 1 — Trigger on a cron

    Cloud Scheduler fires a Cloud Function or Cloud Run job hourly. This is the heartbeat that mints Apple's short-lived JWT and hits Google's live API before that seven-day window drops anything.

  2. 2

    Step 2 — Pull both sources

    Read new rows from Apple's customerReviews and Google's reviews.list (plus the latest GCS CSV for the monthly backfill). Expect overlap on every run; you'll de-duplicate downstream, not here.

  3. 3

    Step 3 — Normalize to one shape

    Map Apple fields and Google fields onto the unified schema and write newline-delimited JSON to a GCS staging prefix. Everything downstream depends on not carrying two schemas forward.

  4. 4

    Step 4 — Load, then MERGE

    Load the NDJSON into a staging table, then MERGE into reviews.unified on review_id. Because a user can edit a review, the MERGE updates the body and re-stamps ingested_at instead of inserting a second copy.

  5. 5

    Step 5 — Classify (the part BigQuery won't do)

    Run a model over the new rows to tag theme and sentiment and write them back. The warehouse stores text and numbers; it does not decide that forty reviews are all the same login bug. That step is yours.

The failure modes that silently lose data

Most of these don't throw an error — they just leave gaps. Google's reviews.list only returns ~the last 7 days, so a gap in your cron means those reviews are gone with no backfill. The Play CSVs are UTF-16, so a UTF-8 loader mangles non-Latin text without complaining. Apple's JWT expires in 20 minutes, so a job that reuses a token 401s mid-run. And without a MERGE on review_id, overlapping pulls double-count every review in your rating math. Build all four in before you trust a single query.

What you can actually query — and what's still missing

Once the table is populated, the volume-and-rating questions are a one-liner. Low-star trend by store over the last quarter:

sql
SELECT
  DATE_TRUNC(DATE(created_at), WEEK) AS week,
  store,
  COUNTIF(rating <= 2)                     AS low_star,
  COUNT(*)                                 AS total,
  ROUND(COUNTIF(rating <= 2) / COUNT(*), 3) AS low_star_rate
FROM reviews.unified
WHERE created_at >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 90 DAY)
GROUP BY week, store
ORDER BY week DESC;
Ratings and counts fall out of raw SQL. Themes do not — that column only exists if step 5 filled it.

Notice the ceiling. BigQuery counts stars and slices dates beautifully, but it can't tell you *why* the 1-stars spiked this week — that needs the classification pass, and "why" is usually the actual question. Turning raw review text into themes, sentiment, and embeddings is a project of its own; [app review data for an AI pipeline](/blog/app-review-data-api-for-ai) walks the normalize-classify-embed loop end to end. It's real work, and it never stops, because reviews keep arriving.

When you don't need the warehouse at all

Building this is the right call when review data has to live next to revenue, retention, and everything else your BI stack already joins on. But look at what steps 1 through 5 were really for. Most teams don't want the rows — they want the answer: what's driving the 1-stars this month, which theme is growing, whether the sync bug is fixed. If that's the consumer, you've stood up an ETL system to power a question an agent could answer directly.

[ReplyArgus](/features) already does the part BigQuery leaves to you. It watches your App Store and Google Play reviews in one inbox, classifies each one, and clusters recurring complaints into a themed roadmap board — the exact classification-and-theme step that is yours to own in a raw pipeline. It doesn't dump raw rows into BigQuery, and it's honest about that: it covers Apple App Store and Google Play only. What it gives you instead are two ways past the pipeline.

  • Ask through the MCP connector — the [ReplyArgus MCP server](/blog/app-reviews-mcp) exposes your reviews to an agent as callable tools: a live cross-store feed filtered by rating, language, or keyword, a ranked theme breakdown with counts, and grounded drafting. For the "what's driving the 1-stars" class of question, the agent queries structured, already-clustered data with no warehouse behind it. The [agentic tools](/agentic-tools) page lists the full toolset.
  • Bridge the themes into BigQuery via Sheets — ReplyArgus exports its roadmap board to Google Sheets, and BigQuery can define an external table over a Google Sheet as a Drive data source. So the clustered themes, already computed, become a queryable BigQuery table you can join against your other data — without you writing or maintaining the classifier that step 5 demanded.

And the same structured feed that answers your questions also drafts replies, grounded in your past approved ones and written in the reviewer's own language. That's the line between a warehouse, which only reads, and a review system, which reads and responds:

Login broke after the July update. I get 'session expired' every single time and can't even open the app anymore. Two stars until this is fixed.

Reply

A login loop that locks you out entirely is exactly the kind of thing that earns two stars, and we're sorry. The 5.1 update shipped a token-refresh bug that's hitting a slice of accounts; a fix is already in review. If you force-close and reopen once, the new session should hold — and if it doesn't, reply here and we'll clear the stuck token on our side today.

Warehouse and agent aren't either/or

Plenty of teams run both: the BigQuery pipeline above for BI and long-term analytics they own end to end, and the MCP feed for the day-to-day "what's happening in reviews" questions and the drafting. The warehouse keeps the raw dataset; the agent skips the plumbing for the questions that need answering now.

Frequently asked

How do I get Google Play reviews into BigQuery?
Two paths. The cheapest is the Cloud Storage export: Play Console automatically writes review report CSVs to a GCS bucket (shown under Download reports > Reviews), and you copy, re-encode from UTF-16, and bq load them. For near-real-time data, call the Play Developer API's reviews.list, normalize the JSON, and stream it in — but that endpoint only returns roughly the last seven days, so it's a live feed, not a full history.
Is there a native BigQuery connector for app reviews?
No. Neither Apple nor Google offers a first-party connector that pipes reviews straight into BigQuery. Google gets you closest by auto-exporting review CSVs to a Cloud Storage bucket you then load; Apple has no export at all, so you pull the App Store Connect customerReviews API and load the results yourself.
Why do my Play Console review CSVs look garbled in BigQuery?
Play Console report CSVs are UTF-16 encoded, and BigQuery's CSV loader expects UTF-8 by default. Re-encode the file first (for example, iconv -f UTF-16 -t UTF-8) before running bq load, or non-Latin characters and the header row will come through corrupted.
How do I avoid duplicate reviews when I re-run the pipeline?
Give each review a stable, store-prefixed review_id and MERGE new pulls into your table on that key instead of INSERTing. Both feeds overlap on every run, and users can edit reviews after posting, so a MERGE updates the existing row (and re-stamps ingested_at) rather than piling up copies that would skew your rating counts.
Can BigQuery tell me what themes are in my reviews?
Not on its own. BigQuery stores and aggregates the text — it counts stars and slices by date perfectly — but it can't decide that forty reviews describe the same bug. That requires a classification pass with an LLM that writes theme and sentiment columns back into the table, or a tool like ReplyArgus that clusters reviews into themes before they ever reach a warehouse.
Do I need a data warehouse to analyze app reviews at all?
Only if reviews must live beside your other BI data. If the consumer is an agent or an analyst asking questions, ReplyArgus's MCP connector hands over reviews already normalized across both stores and clustered into themes, so you skip the poller, the two auth flows, and the classifier. You can also bridge its themed board into BigQuery through a Google Sheets external table if you want it queryable there.

Warehouse the raw reviews if they belong next to the rest of your data — the pipeline above is real, and it's yours to run. But if what you actually need is the answer, or the reply, [start free — no card](/signup), connect a store, and ask your agent to cluster this week's reviews or draft its first response in minutes.

Try it

Let Argus draft your next reply.

Watch it answer a real review in your voice. 10-day trial, no card to begin.

See the features or pricing.

Keep reading