Google Maps Photos Scraper - All Place Photos & Videos
Pricing
from $0.70 / 1,000 photos
Google Maps Photos Scraper - All Place Photos & Videos
[💵 $1.2 / 1K] Every photo and video for any Google Maps place, from a search query or a place URL - no second Actor needed. One flat row per image with direct URL, size, caption, upload date, media type, geotag and camera heading.
Pricing
from $0.70 / 1,000 photos
Rating
0.0
(0)
Developer
WebData Labs
Maintained by CommunityActor stats
1
Bookmarked
4
Total users
3
Monthly active users
20 hours ago
Last modified
Categories
Share
Google Maps Photos Scraper - Every Photo of a Place, Not One Tab
Pull the full photo and video library of any Google Maps place - direct image URLs, sizes, captions, upload dates, coordinates and camera heading - starting from a plain search query.
Google splits a place's photos across tabs it composes per place, and every other photo scraper makes you supply place URLs it cannot find on its own: you run a Google Maps scraper first, export a column, paste it in, and hope the tab you got is the one you wanted. This Actor takes restaurants in Austin TX, resolves the places itself, sweeps every photo tab, deduplicates by Google's own photo id, and writes one flat row per image with the fields you would otherwise have to re-derive by hand.
✅ What you get / ❌ what this isn't
| ✅ This Actor gives you | ❌ This Actor is not |
|---|---|
| A search query is enough to get photos | Not a second Actor you must chain behind a Maps scraper |
| Every photo tab swept and deduplicated | Not one tab's worth of photos passed off as the library |
| One flat row per image, ready for CSV or ML | Not a single nested blob you must flatten yourself |
latitude, longitude and camera heading per photo | Not a bare URL list with the location stripped out |
mediaType separating videos from photos | Not videos silently padding your photo count |
| A README cost table that matches the configured price | Not a listing that advertises one price and bills another |
🔎 Why use this Actor
- Search is built in.
searchQueriesresolves places through Google Maps itself, socoffee in Austin TXis a complete input. No place ids, no CSV round trip. - The whole library, not one tab. Google composes photo tabs per place - a restaurant and a hotel do not even use the same set - so the Actor sweeps all of them and dedupes on
photoIdrather than trusting a fixed tab number. - Every identifier a buyer actually has. Full Maps URLs,
maps.app.goo.glshare links,place_id:ChIJ…URLs, and raw hex CIDs all resolve. Coordinate-only URLs are rejected with an explanation instead of silently returning nothing. - Geotag and camera heading on every photo.
latitude,longitudeandheadingcome straight from Google's own camera pose block - no competing photo Actor returns them. - Two output shapes.
outputMode: "photo"gives one row per image for spreadsheets and training sets;outputMode: "place"nests photos under their place for joining back to a places export. - Videos are labelled, not hidden.
mediaTypeisphotoorvideo, andphotoCategories: ["videos"]keeps only the clips. - Cost-safe default, no hard ceiling. 100 photos per place by default so a first run stays cheap;
maxPhotosPerPlace: 0lifts the limit entirely. - Parallel by place, automatically. Big lists are spread across several workers, each on its own proxy session, with the worker count chosen from the size of the job: measured 111 s for 20 places against 225 s scraping them one at a time. Nothing to tune.
👥 Who it's for
Delivery and directory teams, hospitality and real-estate researchers, brand monitors, and ML engineers who need location imagery at volume. Common jobs:
- Pull menu photos for every restaurant in a city to build or enrich a delivery catalogue.
- Collect interior and exterior shots for every hotel or venue in an area for a listings site.
- Build a labelled, geotagged image set for a computer-vision model.
- Track what customers photograph at each branch of a chain, week over week.
- Source authentic location imagery for travel guides and city pages.
Example tasks
- Scrape Google Maps photos for any search query
- Get restaurant menu photos from Google Maps
- Scrape hotel photos from Google Maps
- Download Google Maps photos from place URLs
- Get geotagged Google Maps photos with camera heading
- Scrape videos from Google Maps places
- Build an image training set from Google Maps places
- Google Maps Fotos scrapen (Deutschland)
⚙️ How to scrape a place's photos
- Open the Actor on Apify.
- Enter
searchQuerieslikepizza in New Yorkorhotels in Lisbon- one query per line. - Set
maxPlacesPerQueryfor how many places each query should contribute. - Optionally paste your own
placeUrlsorplaceIdsinstead - search mode is then skipped for those. - Choose
imageSize(thumbnail,medium,large,original) and, if you only want part of the library, setphotoCategories. - Leave
maxPhotosPerPlaceat 100 for a cheap first run, or set0for everything Google has. - Click Start.
- Open the Photo gallery view for previews, the Geotagged photos view for mapping, or export CSV/JSON/Excel or pull from the API.
Scraping a big list of places
Paste the url column from any Google Maps scrape into placeUrls. The Actor scales its own parallelism to the size of the list, so a few hundred places finish in a fraction of the sequential time, and one unreachable place reports its own status instead of failing the run.
📥 Input
{"searchQueries": ["pizza in New York"],"maxPlacesPerQuery": 5,"maxPhotosPerPlace": 100,"photoCategories": ["all"],"imageSize": "large","outputMode": "photo"}
searchQueries- Google Maps searches to resolve into places. A query is a complete input on its own.placeUrls- place URLs: full Maps URLs with a!1s0x…:0x…segment,place_id:ChIJ…URLs, ormaps.app.goo.gl/goo.gl/mapsshare links. Coordinate-only URLs (@lat,lng,zoom) are not valid - click the place pin first so the URL picks up its place id.placeIds- hex CID identifiers (0x…:0x…) when you already have them.photoCategories- what to keep:all(default),menu,exterior,photos,videos. Every tab is swept either way; this narrows the dataset and the bill.maxPhotosPerPlace- default100,0means unlimited. No hard ceiling.maxPlacesPerQuery- default5. Ignored when you pass place URLs or ids.imageSize-thumbnail(~150px),medium(~800px),large(~1600px) ororiginal(default).outputMode-photo(default, one row per image) orplace(one row per place, photos nested).language,country- locale for captions and which Google frontend to query, defaultEnglish/United States. 18 languages and 21 countries in the dropdowns.
Proxying is handled internally and is not an input: these endpoints block by exit IP, and the Actor's session warming, pacing and rotation are tuned for that. There is nothing for you to configure.
📤 Output
| placeName | imageUrl | mediaType | width x height | uploadDate | latitude, longitude |
|---|---|---|---|---|---|
| Joe's Pizza Broadway | lh3.googleusercontent.com/p/AF1Q…=w1600-h1600-k-no | photo | 3000 x 4000 | 2023-06-08 | 40.75468, -73.98703 |
| Joe's Pizza Broadway | lh3.googleusercontent.com/p/AF1Q…=w1600-h1600-k-no | video | 1920 x 1080 | 2023-07-03 | 40.75468, -73.98703 |
| Secret Patio Lisbon | lh3.googleusercontent.com/p/AA0Q…=w1600-h1600-k-no | photo | 1024 x 683 | 2026-03-16 | 38.71227, -9.13939 |
Every field on a photo row
| Field | Type | What it is |
|---|---|---|
photoId | string | Google's own id for the image. Stable across runs - the right key to deduplicate, diff and join on. |
imageUrl | string | Direct image URL at the requested resolution. |
mediaType | string | photo or video. |
caption | string | Google's caption or alt text, where one exists. |
width | number | Pixel width of the image. |
height | number | Pixel height of the image. |
latitude | number | Latitude the photo was taken at. |
longitude | number | Longitude the photo was taken at. |
heading | number | Camera bearing in degrees, 0 = north. |
uploadDate | string | Upload date as YYYY-MM-DD. |
category | string | Readable category where Google's tag is verified: menu or exterior. null otherwise. |
categoryTag | number | Google's raw category id, so you can group by it yourself. |
tabId | number | Which of the place's photo tabs the image came from. |
placeId | string | Hex CID of the place, e.g. 0x89c259ab3c1ef289:0x3b67a41175949f55. |
placeName | string | Place name, when the place came from a search. |
address | string | Street address, when the place came from a search. |
searchQuery | string | The query that found this place, null in URL or ID mode. |
scrapedAt | string | ISO 8601 timestamp of the scrape. |
Fields on a place row (outputMode: "place")
| Field | Type | What it is |
|---|---|---|
placeId, placeName, address, searchQuery | string | Same as above, at place level. |
sourceUrl | string | The URL or ID you supplied for this place. |
photosCount | number | Photos returned for this place. |
photosCountByCategory | object | Breakdown by category and media type. |
totalPhotosAvailable | number | The largest count Google reported across the place's tabs. |
status | string | ok, no_photos_found, or the error that place hit. |
photos | array | The photo objects, with every per-photo field above. |
scrapedAt | string | ISO 8601 timestamp of the scrape. |
About categories: Google tags every photo with a category id, returned raw as categoryTag. Two ids are verified by content and get a readable category - menu and exterior. The rest come through as numbers with category: null instead of a guessed label, because a confidently wrong category is worse than an honest number.
💵 How much does it cost?
$1.20 per 1,000 photos - you pay per photo actually written to the dataset, not per place scanned or per request made. A place with 5 photos costs 5 photos' worth. In search mode there is a second charge of $2.00 per 1,000 places resolved, which covers the Maps lookup that turns your query into places; paste your own URLs or ids and that event is never charged. There is no Actor start fee.
| Job | Cost |
|---|---|
| 20 places x 100 photos, place URLs supplied | $2.40 |
| 20 places x 100 photos, from search queries | $2.44 |
| 10,000 photos | $12.00 |
| 100,000 photos | $120.00 |
A typical restaurant publishes 40-200 photos and a busy hotel several hundred, so budget from the totalPhotosAvailable field a first capped run reports back. Apify platform usage (compute, proxy, storage) is billed separately by your plan and measured at roughly $0.06 per 1,000 photos.
🌍 Languages and countries
language sets the language of captions and labels Google returns. Supported values:
- English
- German (Deutsch)
- French (Francais)
- Spanish (Espanol)
- Portuguese (Portugues)
- Italian (Italiano)
- Dutch (Nederlands)
- Polish (Polski)
- Turkish (Turkce)
- Russian
- Ukrainian
- Arabic
- Hindi
- Indonesian
- Japanese
- Korean
- Chinese (Simplified)
- Chinese (Traditional)
country sets which Google frontend the Actor queries, which changes what a search returns:
- United States
- United Kingdom
- Canada
- Australia
- Germany
- France
- Spain
- Italy
- Netherlands
- Portugal
- Brazil
- Poland
- Turkey
- Mexico
- Argentina
- India
- Indonesia
- Japan
- South Korea
- United Arab Emirates
- South Africa
Place URLs and place IDs work worldwide regardless of these two settings - they address a specific place, not a regional index.
🔁 Run it on the Apify platform
Schedule it daily or weekly, call it from the API or any Apify client, and export to JSON, CSV, Excel or XML. Webhooks fire on finish, and the dataset connects to Make, Zapier, Google Sheets, Slack and your own endpoints.
curl -X POST "https://api.apify.com/v2/acts/webdata_labs~google-maps-photos-scraper/runs?token=YOUR_TOKEN" \-H "Content-Type: application/json" \-d '{"searchQueries":["pizza in New York"],"maxPlacesPerQuery":5,"maxPhotosPerPlace":100}'
🤖 Use with AI agents via MCP
The Actor is exposed over the Model Context Protocol, so an agent can call it as a tool and get structured photo rows back without any glue code. A search query is a complete input, which is what makes it agent-friendly: the agent never has to construct a Google Maps URL.
Setup for Claude Code
$claude mcp add apify --transport sse "https://mcp.apify.com/sse?actors=webdata_labs/google-maps-photos-scraper" --header "Authorization: Bearer YOUR_APIFY_TOKEN"
Setup for Claude Desktop or Cursor
Add this to the MCP configuration file (claude_desktop_config.json for Claude Desktop, .cursor/mcp.json for Cursor):
{"mcpServers": {"apify": {"url": "https://mcp.apify.com/sse?actors=webdata_labs/google-maps-photos-scraper","headers": { "Authorization": "Bearer YOUR_APIFY_TOKEN" }}}}
Prompts that work
- "Get the menu photos for the top 10 pizza places in New York and list the ones uploaded this year."
- "Pull every photo for this Google Maps place URL and tell me how many are videos."
- "Collect photos for five hotels in Lisbon and give me the coordinates and camera heading for each shot."
- "Build me a CSV of restaurant exterior photos in Austin, original resolution only."
🔗 Use this Actor in n8n
n8n ships an Apify node, so this fits into a workflow without custom code.
- Add the Apify node and connect it with your Apify API token.
- Set the resource to Actor and the operation to Run Actor.
- Pick
webdata_labs/google-maps-photos-scraper. - Paste the input JSON, for example
{"searchQueries":["hotels in Lisbon"],"maxPlacesPerQuery":10}. - Enable Wait for finish, then add a Get dataset items step to pull the rows.
- Send the rows onward - Google Sheets, Airtable, a database node, or an HTTP request that downloads each
imageUrl.
For scheduled collection, put an n8n Schedule Trigger in front of it, or use an Apify schedule and have n8n subscribe to the run-finished webhook instead.
⚠️ Limits and caveats
- Google decides how many photos a place exposes. Each tab is served complete in one response and there is no working pagination past it, so if Google publishes 70 photos for a place, 70 is what exists to collect however high you set
maxPhotosPerPlace. - The Actor returns image URLs, not image files. Downloading and storing the images is your side of the job.
- Captions are sparse - roughly a third of photos have one. Dimensions, media type, upload date and coordinates are present on essentially every item.
- Only two category labels are backed by evidence. Everything else is an honest
categoryTagnumber. totalPhotosAvailableis Google's own count for the busiest tab, so a full sweep can legitimately return more rows than that number.- A place with no photos returns a
no_photos_foundrow, not silence, and one unreachable place never fails the whole run. - Google rate-limits these endpoints by exit IP. The Actor paces requests, warms and rotates its own proxy sessions, and retries. That is handled internally, not exposed as an input.
🧩 Related Actors
- Google Maps Business Scraper API - pull the places first, then feed their URLs in here for imagery.
- Google Maps Email Leads Scraper - contact details for the same places whose photos you are collecting.
- Google Maps Reputation Leads Scraper - find places whose ratings need work, then audit how they present themselves visually.
- Google Images Scraper - the same job for open-web image search rather than a place page.
- Google Search Results Scraper - SERP data to pair with the visual side of a location audit.
⚖️ Is it legal to scrape Google Maps photos?
This is the question buyers ask most, so here is the honest answer rather than a disclaimer.
What the Actor reads. Only data Google serves publicly to any browser, with no login and no account. Nothing behind authentication, nothing private, no personal data beyond what a business itself publishes on its own listing.
What it returns. Image URLs, not image files. The photos stay on Google's servers; the Actor never downloads or rehosts them. If you fetch and store the images afterwards, that step is yours and so is the responsibility for it.
Who owns the photos. Copyright stays with whoever uploaded each one - the business owner or a customer. Collecting a URL for analysis, cataloguing, monitoring or model training is a different act from republishing the image, and only you know which you are doing. Clear republication rights yourself.
Where the lines are. Public data scraping has repeatedly been held lawful in the US (hiQ v. LinkedIn), and in the EU the GDPR question turns on whether you process personal data and on what basis. Business listing imagery generally is not personal data, but a photo can contain identifiable people, so if you republish or run face recognition on it, that is your processing to justify.
What is not acceptable. Do not use the output for harassment, impersonation, passing someone's photos off as your own, or anything unlawful in your jurisdiction. If a rights holder asks you to stop using their image, stop.
None of the above is legal advice. If your use case is commercial republication at scale, ask a lawyer about your specific case rather than trusting a README.
❓ FAQ
Do I pay for places that have no photos?
Not for photos, because none were written. In search mode the place-resolution event still applies, since the lookup happened; in URL or ID mode nothing is charged for that place at all.
Can I get photos from just a place name?
Yes - that is what searchQueries is for. You can also pass a Maps share link or a place_id:ChIJ… URL if that is what you have.
Which photo tab do I get?
All of them. The Actor sweeps every tab Google composed for that place and deduplicates by photo id, so you never have to know how Google organised a particular listing.
Are videos included?
Yes, labelled with mediaType: "video". Use photoCategories: ["videos"] for clips only or ["photos"] to exclude them.
Can I get the original full-resolution images?
Yes - imageSize: "original" is the default and returns the highest resolution Google stores. Street View panorama URLs are returned untouched, since their format does not accept a resize.
Do I need a Google API key?
No. There is no API key, no Google Cloud project, no billing account and no quota to manage. The Actor talks to the same public endpoints the Maps website uses.
How do I download the actual image files?
Take the imageUrl column and fetch each URL - any HTTP client, n8n node or scripting language will do. Set imageSize first so you download the resolution you actually want rather than resizing later.
Why did a place return fewer photos than Google shows?
Google decides how many photos it exposes per tab, and there is no pagination past that. totalPhotosAvailable tells you what Google reported, so you can see the ceiling rather than guess at it.
Can I run this on a schedule?
Yes - use an Apify schedule, or trigger it from n8n, Make or your own cron through the API. Photo libraries change slowly, so weekly is usually enough to catch new uploads.
📝 Changelog
0.2 - 2026-07-30
- Fixed:
imageSize: "original"returned thumbnail URLs. Google's response already carries aw203-h152render spec, and the default setting was passing it straight through - so "original" meant a 203x152, 18 KB image. Original now asks Google for the full file: the same three photos came back at 1.9 MB, 1.7 MB and 3.8 MB. - Fixed: Runs that reached their maximum charge could continue scraping and writing unpaid photos. Photo rows are now written atomically with their charge, nested output is limited to paid photos, and the Actor stops when the spending limit is exhausted.
- Removed the parallelism setting from the input. The Actor now scales workers to the size of the job by itself.
languageandcountryare dropdowns with readable names instead of code fields, covering 18 languages and 21 countries.- Input form grouped into Sources, Filters, Output and Localization.
- Added MCP and n8n integration guides, a full output field table, and this changelog.
0.1 - 2026-07-29
- First release. Search queries, place URLs, share links,
place_id:URLs and hex CIDs as input. - Whole-library sweep across every photo tab with deduplication by photo id.
- Flat and nested output modes, geotag and camera heading per photo, media type labelling, upload dates.
- Per-place error isolation: one unreachable place never fails the run.
🛠️ Support
Something wrong? Open an issue on the Actor's Issues tab with the run URL, the input you used, and what you expected instead. That is the fastest path to a fix.
⭐ Rate this Actor
If this saved you a chained scrape and a CSV cleanup, please leave a review on the Reviews tab. Review count is the main trust signal buyers use on the Store, and reviews are what tell me which fields and filters to build next. If something is broken, open an issue first - I would much rather fix it than have you rate around it.