Appearance
SOP: Local SEO Data Collection Fresh
Local SEO requires data from three overlapping sources: the local pack (map results), individual business data (reviews, attributes), and the local finder (expanded local results). This SOP covers all three.
Data Sources Overview
| Source | API Section | What You Get |
|---|---|---|
| Local Pack | SERP API — Google Maps | 3-pack positions, ratings, reviews count |
| Business Data | Business Data API | Full GMB data, reviews, photos, attributes |
| Local Finder | SERP API — Local Finder | Expanded local results beyond the 3-pack |
Step 1: Pull Local Pack Data (3-Pack)
The local pack is the map block that appears for location-intent queries. This shows who ranks in the 3-pack and their metrics.
python
import os
import requests
from requests.auth import HTTPBasicAuth
auth = HTTPBasicAuth(
os.environ["DATAFORSEO_LOGIN"],
os.environ["DATAFORSEO_PASSWORD"]
)
target_keywords = [
"roofing contractor miami",
"roof repair near me",
"emergency roofing miami fl"
]
# Use Google Maps SERP to get local pack data
local_pack_payload = [
{
"keyword": keyword,
"location_code": 1015116, # Miami, FL
"language_code": "en",
"device": "desktop",
"depth": 10
}
for keyword in target_keywords
]
response = requests.post(
"https://api.dataforseo.com/v3/serp/google/maps/live/advanced",
auth=auth,
json=local_pack_payload
)
data = response.json()
# Parse local pack results
for task in data["tasks"]:
keyword = task["data"]["keyword"]
items = task["result"][0].get("items", []) if task.get("result") else []
print(f"\nLocal Pack: {keyword}")
print(f"{'Pos':>4} {'Business Name':<40} {'Rating':>6} {'Reviews':>8}")
print("-" * 65)
for item in items[:10]:
if item.get("type") != "maps_search":
continue
pos = item.get("rank_absolute", "-")
name = (item.get("title") or "")[:39]
rating = item.get("rating", {}).get("value", "-")
reviews = item.get("rating", {}).get("votes_count", 0)
print(f"{pos:>4} {name:<40} {str(rating):>6} {reviews:>8,}")Step 2: Get Detailed Business Data
Once you have business names from the local pack, pull detailed data using the Business Data API.
python
def get_business_data(auth, business_name, location, limit=1):
"""Get full GMB data for a specific business."""
payload = [
{
"keyword": business_name,
"location_name": location,
"language_name": "English"
}
]
response = requests.post(
"https://api.dataforseo.com/v3/business_data/google/my_business_info/live",
auth=auth,
json=payload
)
data = response.json()
task = data["tasks"][0]
if not task.get("result"):
return None
return task["result"][0]
# Example: get data for a specific competitor
biz_data = get_business_data(auth, "Acme Roofing Miami", "Miami, Florida, United States")
if biz_data:
print(f"\nBusiness: {biz_data.get('title')}")
print(f"Address: {biz_data.get('address')}")
print(f"Phone: {biz_data.get('phone')}")
print(f"Website: {biz_data.get('url')}")
print(f"Rating: {biz_data.get('rating', {}).get('value')} ({biz_data.get('rating', {}).get('votes_count')} reviews)")
print(f"Category: {biz_data.get('category')}")
hours = biz_data.get("work_hours", {})
if hours:
print(f"Hours: {hours}")Step 3: Pull Business Reviews
Use the Business Data reviews endpoint to pull recent reviews for sentiment analysis.
python
def get_business_reviews(auth, business_name, location, limit=50):
"""Pull customer reviews for a GMB listing."""
payload = [
{
"keyword": business_name,
"location_name": location,
"language_name": "English",
"depth": limit,
"sort_by": "newest"
}
]
response = requests.post(
"https://api.dataforseo.com/v3/business_data/google/reviews/live",
auth=auth,
json=payload
)
data = response.json()
task = data["tasks"][0]
if not task.get("result"):
return []
reviews = []
for item in task["result"][0].get("items", []):
reviews.append({
"rating": item.get("rating", {}).get("value"),
"date": item.get("timestamp"),
"text": item.get("review_text", ""),
"author": item.get("author_title"),
"owner_reply": item.get("owner_answer")
})
return reviews
reviews = get_business_reviews(auth, "Acme Roofing Miami", "Miami, Florida, United States")
if reviews:
ratings = [r["rating"] for r in reviews if r["rating"]]
avg = sum(ratings) / len(ratings) if ratings else 0
print(f"\nReviews pulled: {len(reviews)}")
print(f"Average rating: {avg:.1f}")
# Distribution
from collections import Counter
dist = Counter(r["rating"] for r in reviews if r["rating"])
for star in [5, 4, 3, 2, 1]:
count = dist.get(star, 0)
bar = "█" * count
print(f" {star}★: {count:>3} {bar}")Step 4: Pull Local Finder Results
The Local Finder shows the expanded list beyond the 3-pack. Useful for tracking positions 4–20.
python
def get_local_finder_results(auth, keyword, location_code, depth=20):
"""Get Local Finder results (expanded local list)."""
payload = [
{
"keyword": keyword,
"location_code": location_code,
"language_code": "en",
"depth": depth
}
]
response = requests.post(
"https://api.dataforseo.com/v3/serp/google/local_finder/live/advanced",
auth=auth,
json=payload
)
data = response.json()
task = data["tasks"][0]
results = []
if task.get("result"):
for item in task["result"][0].get("items", []):
if item.get("type") == "local_finder":
results.append({
"position": item.get("rank_absolute"),
"name": item.get("title"),
"rating": item.get("rating", {}).get("value"),
"reviews": item.get("rating", {}).get("votes_count", 0),
"address": item.get("address"),
"category": item.get("category"),
"phone": item.get("phone"),
"url": item.get("url")
})
return results
finder_results = get_local_finder_results(auth, "roofing contractor miami", 1015116, depth=20)
print(f"\nLocal Finder — Top {len(finder_results)} results:")
print(f"{'#':>3} {'Business':<35} {'Rating':>6} {'Reviews':>8}")
print("-" * 57)
for r in finder_results[:20]:
name = (r["name"] or "")[:34]
rating = r["rating"] or "-"
print(f"{r['position']:>3} {name:<35} {str(rating):>6} {r['reviews']:>8,}")Step 5: Combine for Local SEO Audit
Merge all three data sources into a single competitive view.
python
def build_local_seo_report(target_domain, local_pack_items, finder_results, biz_data, reviews):
"""Combine data sources into a unified local SEO snapshot."""
# Find target in local pack
target_pack_position = None
for item in local_pack_items:
if target_domain.lower() in (item.get("url") or "").lower():
target_pack_position = item.get("rank_absolute")
break
# Find target in finder
target_finder_position = None
for item in finder_results:
if target_domain.lower() in (item.get("url") or "").lower():
target_finder_position = item["position"]
break
report = {
"domain": target_domain,
"pack_position": target_pack_position or "Not in 3-pack",
"finder_position": target_finder_position or "Not in top 20",
"rating": biz_data.get("rating", {}).get("value") if biz_data else None,
"review_count": biz_data.get("rating", {}).get("votes_count") if biz_data else None,
"review_avg_from_api": sum(r["rating"] for r in reviews if r["rating"]) / len(reviews) if reviews else None,
"recent_reviews": len(reviews),
"has_owner_replies": sum(1 for r in reviews if r.get("owner_reply")) > 0
}
print("\nLOCAL SEO SNAPSHOT")
print("=" * 40)
for k, v in report.items():
print(f"{k:<25}: {v}")
return report
# Build the report
report = build_local_seo_report(
target_domain="acmeroofingmiami.com",
local_pack_items=local_pack_items if 'local_pack_items' in dir() else [],
finder_results=finder_results,
biz_data=biz_data,
reviews=reviews
)Location Code Reference for Local SEO
For local SEO work, use the city-level location codes — not country codes. City codes return localized results.
python
# Get location codes for a city
payload = [{"location_name": "Miami,Florida,United States"}]
response = requests.post(
"https://api.dataforseo.com/v3/serp/google/locations",
auth=auth,
json=payload
)
# Or use known codes:
LOCAL_CODES = {
"Miami, FL": 1015116,
"New York, NY": 1023191,
"Los Angeles, CA": 1013962,
"Chicago, IL": 1016367,
"Houston, TX": 1014927,
"Phoenix, AZ": 1023080,
"Philadelphia, PA": 1023273,
"San Antonio, TX": 1026277,
"San Diego, CA": 1014271,
"Dallas, TX": 1014419,
}Key Metrics for Local SEO Audits
| Metric | Green | Yellow | Red |
|---|---|---|---|
| Local pack position | 1–3 | 4–10 | Not ranked |
| Rating | 4.5+ | 4.0–4.4 | < 4.0 |
| Review count | 100+ | 25–99 | < 25 |
| Owner reply rate | > 80% | 40–80% | < 40% |
| Reviews in last 30 days | 5+ | 1–4 | 0 |