Skip to main content
New

The YouTube API is live.

Try it
Python tutorial

Find reputation-management prospects with unanswered Google reviews

Turn a category and city into a short, traceable list of accounts worth a closer look.

By the Tapline teamLast updated 9 min read

If you sell reputation-management services, a generic list of local businesses gives you no reason to contact one company before another. This tutorial shows you how to enter a business category and city and get back a prospect list of businesses with a recent low-star Google review that had no owner response when captured. Use the linked review to find businesses that may need help managing their online reputation and make your outreach specific.

Would you rather have an agent handle this? Copy the prompt and paste into your favourite LLM to find you leads effortlessly.

Short version

  • Input a business category and city and get back a list of prospects to reach out to.
  • Return businesses where a recent low-star Google review had no owner response when captured.
  • Use the linked review to find prospects who may need help improving and managing their online reputation.

Two accounts from a dentists search in Austin

This example uses dentists in Austin, TX scraped on 2026-11-03. Swap in yours when you run the script.

Business and public contactPlace snapshotExact review evidenceOwner response
Swish Dental SouthWebsite+1 512-296-2841Dentist
4.6 rating
964 reviews
1 star · October 30, 2026“I was ready to move on and let it be water under the bridge but then I started receiving a bill for 241.80.”Absent in captured response
38th Street DentalWebsite+1 737-260-5523Dentist
5.0 rating
734 reviews
1 star · September 15, 2026“Very disappointed with my experience. I felt the office was deceptive about what my insurance would cover, resulting in unexpected costs.”Absent in captured response

What you need to run this

  • Python 3.
  • A Tapline account and API key.
  • The tested client version, installed with pip install "tapline==0.2.19".
  • The environment variable TAPLINE_API_KEY set to your key.

Lets start with a simple example

This request runs a Google Maps search and prints the name, feature id, rating, and review count for three businesses.

terminal
python3 -m venv .venv
source .venv/bin/activate
pip install "tapline==0.2.19"
export TAPLINE_API_KEY="your-api-key"
first_request.py
import os
from tapline import SyncTaplineClient
with SyncTaplineClient(api_key=os.environ["TAPLINE_API_KEY"]) as tapline:
page = tapline.google_maps.search(query="dentists in Austin, TX", limit=3)
for place in page.results:
print(place.name, place.feature_id, place.rating, place.review_count)
output
ATX Family Dental 0x8644b4eb0b01986f:0x6b241d4769ed491e 4.9 915
South Austin Dentist 0x865b4b5e859f967f:0x38deff12e0bdefcb 5.0 181
Austin Lifetime Dental 0x8644cc20724ee6f5:0x2ae9fa2c2bf15149 4.9 846

Build the list

The script first searches your category and city, then extracts the first five businesses that have a source URL, a feature id, and a public phone or website. It then fetches the latest 20 reviews and filters for the ones with the lowest ratings.

find_unanswered_reviews.py
"""Build a dated manual account-review queue from Google Maps review evidence."""
from __future__ import annotations
import argparse
import csv
import os
from dataclasses import asdict, dataclass
from datetime import datetime, timedelta, timezone
from pathlib import Path
from tapline import SyncTaplineClient
SEARCH_LIMIT = 10
MAX_REVIEWED_BUSINESSES = 5
REVIEWS_PER_BUSINESS = 20
MAX_REVIEW_RATING = 3
RECENT_DAYS = 180
SEARCH_CREDITS = 2
REVIEWS_CREDITS = 2
OUTPUT_PREFIX = "google-maps-account-review-queue"
@dataclass(frozen=True)
class AccountReviewEvidence:
business_name: str
category: str
address: str
phone: str
website: str
google_maps_url: str
place_rating: float | None
place_review_count: int | None
review_id: str
review_rating: int
review_created_at: str
review_text: str
review_url: str
owner_response: str
example_filter: str
captured_at_utc: str
def parse_created_at(value: str) -> datetime:
return datetime.fromisoformat(value.replace("Z", "+00:00")).astimezone(timezone.utc)
def public_phone(place: object) -> str:
phone = getattr(place, "phone", None)
if not phone:
return ""
return phone.international or phone.number or ""
def public_website(place: object) -> str:
website = getattr(place, "website", None)
return website.url if website and website.url else ""
def maps_url(place: object) -> str:
if getattr(place, "url", None):
return place.url
if getattr(place, "cid", None):
return f"https://www.google.com/maps?cid={place.cid}"
return ""
def write_csv(rows: list[AccountReviewEvidence], output_dir: Path, run_at: datetime) -> Path:
output_dir.mkdir(parents=True, exist_ok=True)
timestamp = run_at.strftime("%Y%m%dT%H%M%SZ")
output_path = output_dir / f"{OUTPUT_PREFIX}-{timestamp}.csv"
with output_path.open("w", newline="", encoding="utf-8") as output:
writer = csv.DictWriter(output, fieldnames=list(AccountReviewEvidence.__dataclass_fields__))
writer.writeheader()
writer.writerows(asdict(row) for row in rows)
return output_path
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("category", help='Business category, such as "dentists"')
parser.add_argument("location", help='City and region, such as "Austin, TX"')
parser.add_argument(
"--output-dir",
type=Path,
default=Path("."),
help="Directory for the timestamped CSV (default: current directory)",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
run_at = datetime.now(timezone.utc)
cutoff = run_at - timedelta(days=RECENT_DAYS)
query = f"{args.category} in {args.location}"
rows: list[AccountReviewEvidence] = []
reviewed_businesses = 0
with SyncTaplineClient(api_key=os.environ["TAPLINE_API_KEY"]) as tapline:
places = tapline.google_maps.search(query=query, gl="us", limit=SEARCH_LIMIT).results
for place in places:
if reviewed_businesses >= MAX_REVIEWED_BUSINESSES:
break
phone = public_phone(place)
website = public_website(place)
source_url = maps_url(place)
if (
not place.name
or place.is_operating_business is False
or not place.feature_id
or not source_url
or not (phone or website)
):
continue
reviewed_businesses += 1
reviews = tapline.google_maps.list_reviews(
place.feature_id,
sort="newest",
limit=REVIEWS_PER_BUSINESS,
gl="us",
).reviews
matched_for_business = 0
for review in reviews:
if (
review.rating is None
or review.rating > MAX_REVIEW_RATING
or not review.created_at
or not review.text
or not review.url
or review.owner_response is not None
):
continue
if parse_created_at(review.created_at) < cutoff:
continue
matched_for_business += 1
rows.append(
AccountReviewEvidence(
business_name=place.name,
category=place.categories[0] if place.categories else args.category,
address=place.address or "",
phone=phone,
website=website,
google_maps_url=source_url,
place_rating=place.rating,
place_review_count=place.review_count,
review_id=review.review_id or "",
review_rating=review.rating,
review_created_at=review.created_at,
review_text=review.text,
review_url=review.url,
owner_response="absent in captured response",
example_filter=(
f"rating <= {MAX_REVIEW_RATING}; created within {RECENT_DAYS} days; "
"owner_response absent"
),
captured_at_utc=run_at.isoformat(),
)
)
print(
f"REVIEWED | {place.name} | {len(reviews)} newest reviews | "
f"{matched_for_business} matched the example filter"
)
output_path = write_csv(rows, args.output_dir, run_at)
flagged_accounts = len({row.business_name for row in rows})
documented_credits = SEARCH_CREDITS + reviewed_businesses * REVIEWS_CREDITS
print(f"Google Maps returned {len(places)} businesses for {query!r}")
print(
f"Reviewed {reviewed_businesses} operating businesses with public contact and source fields"
)
print(
f"Flagged {flagged_accounts} accounts with {len(rows)} exact review evidence rows"
)
print(
f"API calls: 1 search + {reviewed_businesses} review pages; "
f"documented cost: {documented_credits} credits"
)
print(f"Wrote the dated manual review queue to {output_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

Download find_unanswered_reviews.py

Expand the search

Increase the value of MAX_REVIEWED_BUSINESSES to expand the number of prospects to analyse, and increase SEARCH_LIMIT to raise the number of results we look through. MAX_REVIEW_RATING controls the highest review rating to be returned.

Run it on your category and city

Pass the category and location as arguments, and send output to a folder so each run's CSV stays separate.

terminal
python find_unanswered_reviews.py dentists "Austin, TX" --output-dir final-run
output
REVIEWED | ATX Family Dental | 20 newest reviews | 0 matched the example filter
REVIEWED | Austin Lifetime Dental | 20 newest reviews | 0 matched the example filter
REVIEWED | South Austin Dentist | 20 newest reviews | 0 matched the example filter
REVIEWED | Swish Dental South | 20 newest reviews | 1 matched the example filter
REVIEWED | 38th Street Dental | 20 newest reviews | 1 matched the example filter
Google Maps returned 10 businesses for 'dentists in Austin, TX'
Reviewed 5 operating businesses with public contact and source fields
Flagged 2 accounts with 2 exact review evidence rows
API calls: 1 search + 5 review pages; documented cost: 12 credits
Wrote the dated manual review queue to final-run/google-maps-account-review-queue-20261103T225511Z.csv

Wrapping up

With one script you can now find qualified leads that merit a closer look. Don't forget to update the parameters so that they work for your business.

Google Maps live demo.

Run a live request. No account or API key needed.

Place Search
Places·Reviews

Live Google Maps data · 5 free runs per operation each day

Place Search

Use Run to run this request against live Google Maps data. No API key needed.

Run a request to fetch a live response.
03Pricing

Start free.
Scale when you need to.

Use one credit pool across every live Tapline API. Upgrade only when you need more requests or a higher rate limit.

 

Free

$0
No card required
100 API credits
All endpointsNo charge for failed requests1 req/s
 

Pay As You Go

$9
one-time payment
From 5,000 credits
All endpointsCredits never expire5 req/s
 

Starter

$39
/ month
billed $468/yrSave $120/yr
100,000 credits/mo
All endpointsEmail support5 req/s
 

Enterprise

Custom
Priced to your workload
More than 4,000,000 credits/mo
Higher rate limitsCustom endpointsAnnual contracts
Get started

With Google Maps Data

Point the script at your own category and US city, using 100 free credits on a new account with no card required.

Try Google Maps place search on a real category.
Run live