How to Export Google Reviews to CSV (2 Methods That Actually Work)

Export Google reviews to CSV for any business, not just your own. Two tested methods: a Google Sheets script and a Python exporter, both with real dates.

Every free way to get your reviews into a spreadsheet has the same catch, and nobody puts it in the headline: it only works on businesses you already own.

Google Takeout, the Business Profile API, the review widgets, the $45/month dashboards. All of them authenticate as you.

Which means they stop dead the moment you point them at a competitor, a prospect, or a client who hasn’t added you to their listing yet. If you do local SEO for other people, that’s most of your job.

20

Reviews per request (hard cap)

~2,000

Reviews/month on the free tier

28

Fields per review

2021

How far back our test pulled

So this post covers the two methods that don’t care who owns the profile. One is a script you paste into Google Sheets, the other is Python.

Both run on the same Google Maps reviews API , and the code for each is on GitHub . They also give you the one thing extensions can’t: real dates.

The FlyByAPIs Google Maps Extractor returns reviews for any public business, not just profiles you own. Every review carries a Unix timestamp next to the “3 months ago” string, which is what makes an export sortable. The limit caps at 20 per request, and the free tier covers roughly 2,000 reviews a month.

Everything below ran this week against live data. I pulled 60 reviews for Reading Terminal Market in Philadelphia, then a three-business batch that came back with 80 rows and 13 owner responses.

No cherry-picking. Where something is annoying, I say so.

One honest note before we start, because I don’t want to waste your time.

If you own the profile and just want a one-off dump:

Go to Google Takeout, click "Deselect all", tick Google Business Profile, and create the export. It's free, it's official, and it takes 30 seconds to start. You'll get JSON in a ZIP rather than a spreadsheet, but for a single backup of your own listing it does the job. Everything past this box is for the cases Takeout can't reach.

Why the obvious free routes fall short

Search this topic and you get four suggestions on repeat. Here’s where each one runs out of road.

RouteWorks on others' listings?Where it breaks
Google TakeoutNoJSON in a ZIP, not a spreadsheet. Your own profiles only.
Browser extensionsYesOnly grabs what's loaded on screen, and dates arrive as "a month ago".
Google Places APIYesCapped at 5 reviews per business. Fine for a widget, useless for analysis.
Business Profile APINoFull history and exact dates, but OAuth as the owner. Same wall as Takeout.

The extension problem deserves a second look, because it’s the one that quietly ruins spreadsheets. Reviewflowz, whose help article ranks on page one for this, is blunt about it: exports with relative dates are “pretty much entirely useless”.

They’re right. A column full of “3 months ago” can’t be sorted, can’t be filtered by quarter, and can’t be charted. And it goes stale the day after you export it.

Both methods below fix exactly that. The FlyByAPIs Google Maps Scraper API hands back a Unix timestamp alongside the human string, and one line of code turns it into a date your spreadsheet understands.

What you need before either method

Both methods use the same two endpoints, so the setup is identical.

1

A free API key

Sign up on RapidAPI and subscribe to the free tier of the Google Maps scraping API. 100 requests a month, no card.

2

A business name with its city

"Zahav, Philadelphia" works. Bare "Zahav" is ambiguous and you may resolve the wrong listing.

Under the hood there are two calls. /locate_and_search turns the name into a google_id, the hex string Google uses internally. Then /business_reviews takes that ID and hands back the reviews.

You never have to see the ID. Both scripts chain the calls for you, which is exactly the step most tutorials skip when they just list endpoint URLs.

Method 1: export Google reviews straight into a spreadsheet

No install, no terminal. Google Sheets can call the Google Maps API for review data itself through Apps Script , and the reviews land in the sheet you’re already looking at.

1

Open a new spreadsheet

In the menu bar click Extensions, then Apps Script. A new tab opens with an empty file called Code.gs.

2

Paste the script below

Delete the empty function that's already there, paste everything, then set your key, the business, and how many reviews you want.

3

Click Run and approve the prompt

Google asks for permission the first time because the script talks to an outside service. That's expected.

Here’s the whole thing:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
const RAPIDAPI_KEY = "YOUR_RAPIDAPI_KEY"; // paste your key between the quotes
const BUSINESS = "Reading Terminal Market, Philadelphia"; // name + city
const MAX_REVIEWS = 100; // each page costs 1 request and returns 20 reviews

const API_HOST = "google-maps-extractor2.p.rapidapi.com";
const PAGE_SIZE = 20; // the API caps limit at 20, so we paginate

function exportGoogleReviews() {
  const sheet = SpreadsheetApp.getActiveSheet();

  const business = findBusiness(BUSINESS);
  if (!business) {
    throw new Error("No business found for: " + BUSINESS + " — try adding the city.");
  }

  const reviews = fetchReviews(business.google_id);
  if (!reviews.length) {
    throw new Error("Found " + business.name + " but got no reviews back.");
  }

  const rows = reviews.map(function (review) {
    return [
      business.name || "",
      toDate(review.timestamp),
      review.rating || "",
      (review.text || "").replace(/\n/g, " "),
      review.user_name || "",
      review.user_is_local_guide ? "yes" : "no",
      review.user_reviews_count || 0,
      toDate(review.owner_response_timestamp),
      (review.owner_response_text || "").replace(/\n/g, " "),
      review.url || "",
    ];
  });

  // Only clear once every page came back fine, so a mid-run failure never
  // leaves you with a half-empty sheet.
  sheet.clearContents();
  sheet.appendRow([
    "Business", "Date", "Rating", "Review", "Reviewer",
    "Local guide", "Reviewer total reviews", "Owner replied", "Owner response", "Link",
  ]);
  sheet.getRange(2, 1, rows.length, rows[0].length).setValues(rows);
  sheet.getRange(1, 1, 1, 10).setFontWeight("bold");
  sheet.setFrozenRows(1);

  SpreadsheetApp.getActiveSpreadsheet().toast(
    "Exported " + rows.length + " reviews for " + business.name
  );
}

/** Turn a business name into the google_id the reviews endpoint needs. */
function findBusiness(query) {
  const url = "https://" + API_HOST + "/locate_and_search" +
    "?query=" + encodeURIComponent(query) +
    "&country=us&language=en&limit=1";

  const data = callApi(url).data || [];
  return data.length ? data[0] : null;
}

/** Page through the review history 20 at a time until we hit MAX_REVIEWS. */
function fetchReviews(googleId) {
  let reviews = [];
  let token = "";

  while (reviews.length < MAX_REVIEWS) {
    let url = "https://" + API_HOST + "/business_reviews" +
      "?business_id=" + encodeURIComponent(googleId) +
      "&country=us&language=en&sort_by=mostRecent&limit=" + PAGE_SIZE;
    if (token) {
      url += "&next_page_token=" + encodeURIComponent(token);
    }

    const payload = callApi(url);
    const batch = payload.data || [];
    if (!batch.length) break;

    reviews = reviews.concat(batch);

    token = payload.next_token || "";
    if (!token) break; // no more pages
    Utilities.sleep(500); // stay inside the rate limit
  }

  return reviews.slice(0, MAX_REVIEWS);
}

function callApi(url) {
  const response = UrlFetchApp.fetch(url, {
    headers: {
      "x-rapidapi-host": API_HOST,
      "x-rapidapi-key": RAPIDAPI_KEY,
    },
    muteHttpExceptions: true,
  });

  const code = response.getResponseCode();
  if (code === 401) throw new Error("401 Unauthorized — check your RAPIDAPI_KEY.");
  if (code !== 200) throw new Error("API error " + code + ": " + response.getContentText());

  return JSON.parse(response.getContentText());
}

function toDate(timestamp) {
  return timestamp ? new Date(timestamp * 1000) : "";
}

To switch businesses, change the BUSINESS line and click Run again. The sheet clears and refills.

Why toDate() is the important line:

It turns the Unix timestamp into a genuine Sheets date object, not text. That's what lets you sort chronologically, build a pivot by month, and chart your rating trend. Every export that hands you "3 months ago" fails at exactly this step.

One caution about your key: it sits in the script, so don’t share that spreadsheet publicly with edit access, and never paste the script online with your real key in it. If a key leaks, regenerate it on RapidAPI in one click.

Try the Google Maps API free on RapidAPI →

100 requests/month free · No credit card required

Method 2: the Python script for many businesses at once

Apps Script handles one business at a time. When you’re building a quarterly report across a dozen client locations plus their competitors, you want a loop and a proper CSV.

Setup:

1
2
pip install requests
export RAPIDAPI_KEY="your_key_here"

Then:

1
2
3
4
5
# one business
python export_reviews.py "Reading Terminal Market, Philadelphia"

# a whole client list, one business per line
python export_reviews.py --from-file businesses.txt --out client_reviews.csv
FlagDefaultWhat it does
--max100Maximum reviews per business
--sortmostRecentAlso qualityScore, ratingHighToLow, ratingLowToHigh
--from-file—Text file with one business per line
--outgoogle_reviews.csvOutput path

The pagination loop is the heart of it, and it’s worth reading even if you never run the script, because it’s what every extension gets wrong:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def fetch_reviews(google_id: str, max_reviews: int, sort_by: str) -> list[dict]:
    """Page through every review for one business until we hit max_reviews."""
    reviews: list[dict] = []
    token = None

    while len(reviews) < max_reviews:
        params = {
            "business_id": google_id,
            "language": "en",
            "country": "us",
            "limit": PAGE_SIZE,
            "sort_by": sort_by,
        }
        if token:
            params["next_page_token"] = token

        payload = get_json(REVIEWS_URL, params)
        batch = payload.get("data") or []
        if not batch:
            break

        reviews.extend(batch)
        print(f"  +{len(batch)} reviews ({len(reviews)} so far)")

        token = payload.get("next_token")
        if not token:
            break          # no more pages
        time.sleep(1)      # be polite, stay inside the rate limit

    return reviews[:max_reviews]

And the date conversion, the Python twin of toDate():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def to_iso_date(timestamp) -> str:
    """Turn the Unix timestamp into a real YYYY-MM-DD date."""
    if not timestamp:
        return ""
    try:
        return datetime.datetime.fromtimestamp(
            int(timestamp), tz=datetime.timezone.utc
        ).strftime("%Y-%m-%d")
    except (ValueError, OSError, OverflowError):
        return ""

The full script is in the repo , with 18 columns including reviewer history, owner responses, photo counts, and a direct link to each review. It’s the same pattern we used to pull business listings into a spreadsheet, just pointed at the reviews endpoint instead.

Here’s a real run against three Philadelphia restaurants:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Zahav, Philadelphia
  found: Zahav (3094 reviews total)
  +20 reviews (20 so far)
  +20 reviews (40 so far)

Suraya, Philadelphia
  found: Suraya Restaurant (4155 reviews total)
  +20 reviews (20 so far)
  +20 reviews (40 so far)

zzzznotarealplace9999
  no business found for 'zzzznotarealplace9999' — try adding the city

Wrote 80 reviews to client_reviews.csv

Note the third line. A name with no match comes back empty rather than throwing an error, so one typo in a 40-row client list doesn’t kill the whole run. The business reviews endpoint reference lists every parameter if you want to go deeper.

What you actually get back

Each review carries 28 fields. The ones that actually matter for a client report:

The review itself

Exact date, star rating, full text, photo count, language, and a permalink back to the review on Google.

Context around it

Reviewer name, Local Guide status, how many reviews they've written, plus the owner's response and when it was posted.

That owner response column is the one clients react to, and the Maps API returns it across the full review history rather than only for the newest few. In our three-business run, 13 of 80 reviews had a reply.

Sort by rating with the response column visible and every unanswered one-star surfaces in about four seconds. That’s usually the first slide of the report.

Reply speed is the other thing that falls out of this for free. Both dates are real timestamps, so one column minus the other gives you the average per location, over whatever window you want.

Three limits worth knowing before you run it

I’d rather you hear these from me than discover them at 11pm before a client call.

1

Twenty per request, no exceptions

I sent limit=100 and got 20 back. The cap is real, so budget one request per 20 reviews. A business with 3,000 reviews needs 150 requests to pull in full.

2

A third to a half of reviews have no text

Between 30% and 48% across the businesses we tested were star-only. That's how people rate things, not a bug in the export. Filter the blanks before you count anything.

3

Sorting by newest has odd stragglers

A 2021 review turned up in the middle of a most-recent run. Those are edited reviews: Google reorders them by edit date while the timestamp stays original. Sort your spreadsheet by the date column afterwards and it's a non-issue.

On cost: the FlyByAPIs free tier is 100 requests a month, which is about 2,000 reviews. That covers a handful of locations comfortably.

Past that you’re into paid plans, and the Google Maps data extraction API starts at $19.99/month. That’s still a third of what a single-location review dashboard charges.

Which method should you pick

Pick Google Sheets

One or two businesses, a report due today, and no appetite for a terminal. You'll have data in the sheet within five minutes of pasting the script.

Pick Python

More than three businesses, a recurring report, or you want to diff this month against last. The --from-file flag turns a client list into one CSV.

Most agencies I’ve talked to end up running both. Sheets for the ad-hoc “what are they saying about us” question in a meeting, Python for the monthly deliverable.

Either way you’re hitting the same endpoint, and the Google Maps business data API bills both identically. Switching later costs you nothing.

Review data is rarely the only thing a client report needs, and the same key covers the rest of what we run.

Rank tracking

SERP data for keyword positions, for when the review work turns into a full local audit.

Product data

Amazon product and review data, same idea applied to listings instead of storefronts.

Translation

A translation API, which sounds like overkill until a tourist-heavy listing comes back half in six languages.

The short version

Takeout and the official API both work fine, right up to the moment you need data on a business that isn’t yours. Then they stop, and no amount of configuration gets you past it.

The two scripts here don’t have that ceiling, because the FlyByAPIs endpoint behind them reads public listings instead of authenticating as the owner. Paste one into Sheets or run the other from a terminal, and you get the full review history with dates you can actually sort.

At the volumes most agencies work at, neither one costs you anything. They both live in the code repo , so fork them and add whatever columns your reports want.

Key takeaway: ownership is the only thing that separates these two methods from every other free option. Drop that requirement and the whole problem becomes a pagination loop.

The Google Sheets version is where I’d start. Five minutes, one paste, and you’ll know within a single run whether this solves your problem.

Get your free key and pull your first reviews →

100 requests/month free · No credit card required

P.S. If you extend the script, the field I’d add next is user_reviews_count. A one-star from someone with 400 reviews and Local Guide level 8 is a different animal from a one-star on a brand new account.

That distinction has settled more than one argument about whether a review is worth disputing.

Oriol.

FAQ

Frequently Asked Questions

Q How can I download Google reviews as a CSV file?

Two ways that work without owning the business profile. Paste the Apps Script from this post into Google Sheets and click Run, or run the Python script from the FlyByAPIs code repo. Both call the /business_reviews endpoint of the Google Maps Scraper API, paginate through the full history, and write a spreadsheet with real dates instead of relative strings.

Q How can I export Google reviews to Excel for free?

The RapidAPI free tier gives you 100 requests per month, and each request returns 20 reviews. That's roughly 2,000 reviews a month at no cost, with no credit card. Run either script in this post, then open the resulting CSV in Excel or import it straight into Google Sheets.

Q Can I download reviews for a business I don't own?

Yes. This is the main difference from Google Takeout and the Google Business Profile API, which both require you to own or be granted access to the listing. The Google Maps Scraper API reads public review data, so competitor profiles, prospects, and client listings you haven't been added to all work identically.

Q Does Google Takeout give me a CSV of my reviews?

No. Takeout exports Google Business Profile data as JSON inside a ZIP archive, not as a spreadsheet, and it only covers profiles on your own account. Converting that JSON into usable columns takes more work than most people expect, which is why the tools ranking for this topic all offer a converter.

Q How many reviews can I pull in one request?

Twenty. The limit parameter caps at 20 no matter what value you send, which we confirmed by requesting 100 and getting 20 back. Anything beyond the first 20 requires following the next_token value from each response, and both scripts in this post handle that loop for you.

Q Why do so many reviews have a rating but no text?

Because plenty of people tap a star rating and never write anything. Across the three businesses we tested, between 30% and 48% of reviews had a rating and an empty text field. That's normal Google behavior, not a gap in the export, so filter out the blanks if you only care about written feedback.

Q How to scrape Google reviews for free?

Browser extensions are the usual free answer, and they mostly capture whatever is loaded on screen with dates like 'a month ago'. A free API tier is more reliable: you get the full paginated history, exact timestamps, owner responses, and reviewer detail, without signing into Google or risking your own account.

Q Why do exported reviews show '3 months ago' instead of a real date?

That's the relative string Google renders on the page, and it's all a scraper reading the HTML can see. The API returns both that string and a Unix timestamp, so the scripts here convert the timestamp into a proper date column you can sort, filter by quarter, and chart.
Share this article
Oriol Marti
Oriol Marti
Founder & CEO

Computer engineer and entrepreneur based in Andorra. Founder and CEO of FlyByAPIs, building reliable web data APIs for developers worldwide.

Free tier available

Ready to stop maintaining scrapers?

Production-ready APIs for web data extraction. Whatever you're building, up and running in minutes.

Start for free on RapidAPI