outscraper-pull
Buy or scrape Google data through Outscraper — Maps listings, reviews, popular times, busy hours, SERPs, page-one rank — without overpaying or buying the wrong product. Use when a task needs to pull,
SKILL.mdskills/outscraper-pull/SKILL.md
GitHubPulling Google data through Outscraper
You are spending real money on an API whose response body does not tell you what you were charged, and whose products overlap confusingly. Every rule here is attached to a specific way that cost money. House rule: cache every paid response to disk and never re-buy what you already hold.
THE FREE CHECKS COME FIRST — AND THEY USUALLY ANSWER IT
Run all three before writing a probe, let alone a pull. In practice a paid probe of
popular_times was run first, and two of the three findings that decided the whole load
came out of these free checks afterwards.
1 · Grep your own disk for the field. Keep paid pulls in a shared cache folder (e.g.
data/cache/) so they are never repeated.
head -1 data/places.csv | tr ',' '\n' | grep -in "<field>"
python -c "
import json; d=json.load(open('data/outscraper_reviews.json'))
print(len(d), sum(1 for r in d if r.get('<field>')))"
Why: an old reviews pull held popular_times populated on 54 of 97 places the whole
time it was being called unproven. It also revealed which product carries the field.
A file you already own is a free probe with a bigger sample than you'd buy.
2 · Read the SDK signature; don't trust a doc's claim about it. Open
site-packages/outscraper/client.py. Docs have claimed v3 takes no fields parameter; it
does. Real v1/v3 differences: extract_contacts (v1), skip/async_request/enrichment (v3).
3 · Ask what right you have to republish it. Not a technical question, and it comes before the technical ones. See The rights gate.
WHICH PRODUCT CARRIES WHICH FIELD
The split is by PRODUCT, not by old-vs-new endpoint. Measured on a 400-record probe:
| SDK call | endpoint | bills | popular_times |
|---|---|---|---|
google_maps_search |
POST /google-maps-search |
per record | 0.0% of 400 |
google_maps_search_v1 |
GET /maps/search |
— | no records at all (on the account tested) |
google_maps_reviews |
POST /google-maps-reviews-v3 |
per review | 55.7-66.7% |
google_search |
/google-search-v3 |
per query | n/a — and no map pack |
google_maps_search_v1 may be dead for your account: test it once, cheaply. Exact
names at limit=1, explicit fields, and a plain category query each returned status: Success, one empty group per query, and billed nothing; another hung Pending for
over an hour. No records, not records missing a field.
The reviews response is a PLACE record with reviews_data attached — which is why
place-level detail (popular_times, about, subtypes, working_hours) rides there.
Reach for reviews when you want place detail, not only review text.
reviews_limit=1 decides the bill. It returns the full seven-day curve with one
review attached, billing one unit per place. So popular_times for a metro costs
places x 1, not a review corpus per place.
BILLING: THE RESPONSE BODY DOES NOT TELL YOU WHAT YOU PAID
Read the meter, not the rate card, and never the response length.
python -c "
from outscraper import ApiClient; c=ApiClient(api_key=KEY)
for r in c.get_requests_history(type='finished', page_size=5).get('items',[]):
print(r.get('id'), r.get('usages'))"
It returns {"product_name": ..., "quantity": N} per job — the authoritative answer to
what product did I actually hit and what did it cost. It returns a dict with
items, not a list; indexing it as a list raises TypeError: unhashable type.
It also distinguishes "returned records without the field" from "returned no
records": "usages": [] means zero billed means zero records. Without it, an
empty-but-successful response reads as "the field isn't available" and sends you
hunting fields combinations forever. Check it every time a probe comes back empty.
Rates seen in 2026 (re-check live before a real buy; these age):
| product | rate | free allowance |
|---|---|---|
| Maps search | $3/1k records | 500 records |
| Maps reviews | ~$0.003/review | |
| Google Search | $5/1k queries, $3/1k past 5,000 | 25 queries |
You pay per record, never per field — so always ask for the widest field set. fields
unset means all fields. Free riders measured: working_hours 97.8%, reviews_per_score
98.8% (the full 1-5 distribution), subtypes 99.8%, about 94.2%.
Queries are free; only results cost. A fan-out can be as fine as you like; overlap is
the only thing to engineer against — a business returned by four queries is billed four
times, and dedup happens on your side, after the meter. A deliberately colliding grid
measured 1.03x overlap. Never reuse a gross-up factor from a doc; print it from a
--dry-run.
THE FIELDS THAT LIE
categoryECHOES YOUR QUERY. On 17% of records it came back as the lowercase plural of the search term —'restaurants','bars','coffee shops'. Any trade tally built on it feeds your own query strings back as findings.subtypesis the honest field — 99.8% populated, always Google's own taxonomy.popular_timesis heterogeneous and will crash a naive parser. Most records append an eighth element to a seven-day list:{"day": "live", "percentage": 49, "time": 11}— a live reading whosedayis the string"live". Days run1=Mon...7=Sunwith Sunday first in the list, so positional indexing mis-assigns every day, and a day can carry zero hours. Skip anything withoutday_text, readday['day'], and never let the live reading into a weekly curve.linkis a Google redirect stub on ~18% of SERP results, clustered by query. The real host is usually recoverable fromdescription; refuse rather than guess when it isn't.business_statusis Google's opinion, not the state's. Maps is not a business registry: it over-counts (duplicate pins, ghost listings, home-based sole traders) and under-counts (no listing, most B2B in office parks).- A seed is not an answer. When Maps finds nothing matching it returns neighbours. Drop those rows and count them as unmatched rather than guessing.
THE RIGHTS GATE — ASK BEFORE YOU PRICE, NOT AFTER
This is Google's data, and what you may do with it is a separate question from whether you can fetch it. If you publish data under an open licence (e.g. CC BY 4.0, which grants downstream commercial redistribution), you cannot grant over Google's results what Google's terms do not grant you. Treat any Google-derived layer as unshippable as a download.
It can still power analysis, joins and tools internally. But say that out loud before costing a load, because it changes what the money buys. A buy whose output can never be a file is a decision for whoever owns the budget, not the builder.
HOW TO WRITE A PULL
--dry-runprints the grid and the bill and spends nothing. Non-negotiable on anything above a handful of records. Let the bill print itself; never type it.- Cache to
data/cache/<name>.jsonand refuse to re-pull without--force. - Write a partial file after every chunk. A crash at query 3,000 of 3,060 otherwise means paying for all 3,060 again.
- The response is one list per query IN ORDER, and that ordering is the only join back
to your grid. A length mismatch is
assert-fatal, never skippable. - Chunk size is measured, not chosen. SERP: 25 per chunk, because 100 returns 504 — the queries ride in the URL, so a big chunk is a long GET that times out on submit. Maps search: 100 is fine (POST body).
- Submit
async_request=Trueand collect later — 15 minutes instead of 3 hours. Cap waves (~16 jobs); a deep queue gets an account throttled. - Load the key from the environment and fail naming the variable, never a stack
trace:
sys.exit("OUTSCRAPER_API_KEY missing from .env").
Operational traps:
- The SDK has no cancel method. To kill a hung job,
DELETE /requests/{id}athttps://api.app.outscraper.com— returns200 "terminated", but Outscraper keeps listing the job as running. Acknowledged is not cleared. - Finish background pulls before deleting the environment they run in (a git worktree
with its own venv):
OSError: Could not find a suitable TLS CA certificate bundle. python -u, or you see nothing. A buffered pull prints its whole progress log only on exit, so a 20-minute job looks hung.
Decide yourself vs. bring to the budget owner
Decide yourself: endpoint and product, grid design, chunk and wave sizes, field set, probe scope, whether a probe's answer kills the load.
Bring to them, with the tradeoff and a recommendation: any spend whose output cannot ship; any purchase materially above its scoped price (re-derive it and say so — a load scoped at $168 came in at ~$240 for a different product than quoted).