# Google Maps Photos Scraper: Place Photos, Videos, Owner Uploads (`steadyscrape/google-maps-photos-scraper`) Actor

Scrape the photos, images and videos of any Google Maps business: original-size links, width and height, upload date, owner or visitor, review pictures, video MP4 links, plus an optional photo summary per place for local SEO. By link, CID, name or search. Export to JSON, CSV or Excel, or via API.

- **URL**: https://apify.com/steadyscrape/google-maps-photos-scraper.md
- **Developed by:** [Steady Scrape](https://apify.com/steadyscrape) (community)
- **Categories:** SEO tools, Marketing, Travel
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 photo delivereds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Google Maps Photos Scraper

**Google Maps Photos Scraper** lists the photos and videos of any place on Google Maps — a business, a restaurant, a
hotel, a landmark — the way its photo panel shows them: each photo's link at its original size (or any size you
want), its width and height, when it was uploaded, whether **the owner or a visitor** uploaded it, whether it came with
a review, and each video's MP4 link and length. Give it Google Maps links, CIDs, place ids, names, or a search such as
`dentist in Austin, TX`. Add a **photo summary per place** — Google's own photo count, owner vs visitor photos, the
newest owner upload — for local SEO audits. No login, no browser. Export to JSON, CSV or Excel, or via API.

### What you can do with Google Maps photos

- **Local SEO audits**: see when a business last uploaded a photo to its Google Business Profile and how many of its
  photos come from the owner versus customers — a stale profile is an easy pitch.
- **Competitor research**: compare the photos the top places of a search show, and how fresh they are.
- **Listing and content checks**: find every photo a location uploaded, with its size, to spot low-resolution images.
- **Reputation and UGC monitoring**: follow the photos visitors post with their reviews.
- **Datasets**: build image datasets of places (links and metadata; download the files you are allowed to use).

### How to scrape Google Maps photos

1. Paste one or more places: a Google Maps place link, a `maps.app.goo.gl` share link, a `?cid=` link, a place id
   (`ChIJ…`), a CID, or `name, city`. Or type a search term and a location.
2. Choose how many photos per place, and whether you want photo rows, a summary row per place, or both.
3. Optionally keep only the owner's or visitors' photos, only videos, or only recent uploads.
4. Run it and download the dataset.

#### Input example

```json
{
    "places": ["https://www.google.com/maps/place/Swish+Dental+Downtown/data=!4m2!3m1!1s0x8644b5b434046875:0x644e6eb422388b7a"],
    "searchTerms": ["coffee shop"],
    "location": "Austin, TX",
    "maxPlacesPerSearch": 5,
    "maxPhotosPerPlace": 30,
    "output": "photos_and_summary",
    "uploadedBy": ["owner"]
}
```

### Google Maps photos output

One row per photo or video:

```json
{
    "rowType": "photo",
    "source": "place",
    "input": "https://www.google.com/maps/place/Swish+Dental+Downtown/data=!4m2!3m1!1s0x8644b5b434046875:0x644e6eb422388b7a",
    "inputIndex": 1,
    "searchQuery": null,
    "searchRank": null,
    "placeName": "Swish Dental Downtown",
    "placeId": "ChIJdWgENLS1RIYReos4IrRuTmQ",
    "cid": "7227836171923524474",
    "mapsUrl": "https://www.google.com/maps?cid=7227836171923524474",
    "address": "201 W 5th St Ste 175, Austin, TX 78701",
    "primaryCategory": "Dentist",
    "positionInPlace": 1,
    "photoId": "CIABIhCWUcgEtRg-429FERFJdZlw",
    "mediaType": "photo",
    "imageUrl": "https://lh3.googleusercontent.com/gps-cs-s/ANWiy9Ti4A…=s0",
    "imageBaseUrl": "https://lh3.googleusercontent.com/gps-cs-s/ANWiy9Ti4A…",
    "thumbnailUrl": "https://lh3.googleusercontent.com/gps-cs-s/ANWiy9Ti4A…=w203-h155-k-no",
    "width": 2500,
    "height": 1911,
    "uploadedAt": "2025-07-21T23:00:00.000Z",
    "uploadedBy": "owner",
    "uploadChannel": "bizbuilder:gmb_on_local_universal_desktop",
    "fromReview": false,
    "latitude": 30.2675996,
    "longitude": -97.7448115,
    "videoUrl": null,
    "videoDurationSeconds": null,
    "scrapedAt": "2026-11-03T06:10:00.510Z"
}
```

And, when asked, one summary row per place:

```json
{
    "rowType": "summary",
    "placeName": "Swish Dental Downtown",
    "totalPhotos": 24,
    "photosRead": 13,
    "allPhotosRead": true,
    "ownerPhotos": 12,
    "visitorPhotos": 1,
    "visitorReviewPhotos": 1,
    "videos": 0,
    "streetViewAnd360Photos": 1,
    "newestOwnerUploadAt": "2025-07-22T17:00:00.000Z",
    "daysSinceOwnerUpload": 468,
    "newestVisitorUploadAt": "2026-05-21T22:00:00.000Z",
    "ownerUploadsLast90Days": 0,
    "visitorUploadsLast90Days": 0
}
```

#### Photo fields

| field | what it is |
|---|---|
| `photoId` | Google's id of the photo or video |
| `mediaType` | `photo`, `video` or `photo_360` (a 360° photo someone uploaded) |
| `imageUrl` | the image at its original size (for a video, its still) |
| `imageBaseUrl` | the same link without its size: add `=w800-h600` for a box, `=s0` for the original |
| `thumbnailUrl` | the small image Google Maps' photo list shows |
| `width`, `height` | the original size in pixels |
| `uploadedAt` | when it was uploaded, to the hour (UTC) |
| `uploadedBy` | `owner` (the business) or `visitor` |
| `fromReview` | `true` when a visitor uploaded it with a review |
| `uploadChannel` | Google's name for where it was uploaded from, e.g. `bizbuilder:gmb_web` (Business Profile on the web), `photos:gmm_ios_review_post` (a review from the iPhone app) |
| `latitude`, `longitude` | where the photo is placed on the map |
| `videoUrl`, `videoDurationSeconds` | a video's MP4 link (best quality) and its length |
| `positionInPlace` | its position in the place's photo list (1 = the first Google shows) |
| `placeName`, `placeId`, `cid`, `mapsUrl`, `address`, `primaryCategory` | the place |
| `rowType` | `photo`; `summary` for a summary row; `place` or `search` for a free row (see below) |
| `source`, `input`, `inputIndex`, `searchQuery`, `searchRank` | which input the row answers (`inputIndex` is its line, from 1); for a search, the place's rank |
| `scrapedAt` | when the run read it (UTC) |

#### Summary fields

`totalPhotos` is the photo count Google Maps shows on the place. Every other count is over the photos the run read
(`photosRead`: "Photos per place" rounded up to Google's pages of 10, more when a filter makes the run read on, or
the whole list when it is shorter;
`allPhotosRead` says whether that was the whole list): `ownerPhotos`,
`visitorPhotos`, `visitorReviewPhotos`, `videos`, `streetViewAnd360Photos`, `newestUploadAt`, `newestOwnerUploadAt`,
`newestVisitorUploadAt`, `daysSinceOwnerUpload`, `ownerUploadsLast90Days`, `visitorUploadsLast90Days`, plus the
place's rating, review count, website and phone.

#### Rows that cost nothing

A place that is not found, a link that names no single place, a place without photos, a place with no photo matching
your filters, a search that finds nothing, a Google refusal, or a place the spending limit left out: one free row with
`rowType` `place` or `search`, its `status` (`place_not_found`, `invalid`, `no_photos`, `no_matching_photos`,
`blocked`, `spending_limit`; `no_results` for a search) and the `reason`. The run still succeeds.

### Pricing

Pay per result, no subscription:

- **US$0.001 per photo or video row** (US$1 per 1,000 photos).
- **US$0.002 per place summary**.
- Every free row above costs nothing.

Set a spending limit on the run and it stops at what it pays for.

### Limits and good to know

- **The order is Google's**: the photos come as Google Maps' photo panel lists them ("All"), by relevance, not by
  date. Google lists 10 photos per request. With a date or uploader filter the run reads up to 1,000 photos per place to
  find the matching ones; you pay only for the photos delivered.
- **Google's count and the list differ**: a place's page can say "24 photos" while its photo list shows 14 (Swish
  Dental Downtown, November 2026). `totalPhotos` is Google's count; the rows are what the list shows.
- **Google's own Street View panoramas are not delivered**: their images open only on Google Maps' own page. The
  summary counts them in `streetViewAnd360Photos`.
- **Uploader names are not included**: Google's photo list does not carry them.
- **Only links and metadata**: the Actor never downloads the files. Photos uploaded by visitors are their copyright;
  use them only as the law and Google's terms allow.
- **When Google asks an IP for verification** — its "unusual traffic" page, a CAPTCHA, a JavaScript check, or any
  answer that is not Google Maps' normal data — **that IP is done for the run**: nothing more is sent from it, and the
  run goes on from another IP of Apify's datacenter proxy. It never solves, bypasses or replays a challenge. Plain
  overloads (HTTP 429 or 5xx without a challenge) take the same way. When every IP the run can use has been refused,
  the places left are free `blocked` rows that say so, and the run ends normally with a message. Try again later.

### Is it legal to scrape Google Maps photos?

This Actor reads public data that Google Maps shows to anyone without logging in: photo links, sizes, dates and
whether the owner or a visitor uploaded them. It does not log in, does not solve or bypass any challenge, and does not
download the images. Scraping public data is generally legal, but Google's terms of service restrict automated
access, and the photos themselves belong to whoever took them. Make sure your use complies with the law where you
are and with copyright; ask a lawyer if in doubt.

This Actor is not affiliated with, endorsed by or sponsored by Google. Google Maps is a trademark of Google LLC.

### Related Actors

- **Google Business Profile Audit** — scores a profile (photos, posts, reviews, hours…) against its competitors.
- **Google Maps Business Posts Scraper** — the posts a business publishes ("Updates", offers, events).
- **Google Maps Rank Tracker** — where a business ranks on the map, point by point.

# Actor input Schema

## `places` (type: `array`):

One per line: the place's <b>Google Maps link</b> (the place page, a <code>?cid=</code> link, or a <code>maps.app.goo.gl</code> share link), its <b>place id</b> (<code>ChIJ…</code>), its <b>CID</b>, or its <b>name and city</b> (<code>Swish Dental Downtown, Austin TX</code>). A link or an id is exact; a name is looked up on Google Maps and only accepted when the place found carries that name. Up to 500.

## `searchTerms` (type: `array`):

Find the places on Google Maps first, one search per line, written as you would in Google Maps: <code>dentist</code>, <code>coffee shop</code>, <code>hotels in Lisbon</code>. The photos of the places Google lists first are read. Up to 50.

## `location` (type: `string`):

Written after each search term, as in <code>dentist in Austin, TX</code>: a city, a neighborhood, a ZIP code. Leave it empty when the terms already name the place.

## `maxPlacesPerSearch` (type: `integer`):

How many of the places Google Maps lists first for each search term have their photos read (1–20).

## `output` (type: `string`):

A <b>photo row</b> has the photo's link at its original size, its width and height, upload date, owner or visitor and upload channel. A <b>summary row</b> counts the place's photos: Google's own total, by the owner, by visitors, from reviews, videos, the newest owner and visitor uploads, uploads in the last 90 days.

## `maxPhotosPerPlace` (type: `integer`):

The photos delivered for each place, in the order Google Maps' photo panel shows them, up to 1,000. With <b>Only a summary</b>, how many photos are read to count. Google lists 10 photos per page: 50 photos are 5 requests.

## `uploadedBy` (type: `array`):

Only photos the owner uploaded, or only visitors'. Leave empty for both. Filters choose photo rows; a summary always counts every photo read.

## `mediaTypes` (type: `array`):

Only these kinds. Leave empty for all.

## `uploadedWithinDays` (type: `integer`):

Only photos uploaded in the last N days (<code>90</code> for the last quarter); 0 for any date. Google Maps lists photos by relevance, not date, so with a filter the run reads up to 1,000 photos per place to find them (you pay only for those delivered).

## `language` (type: `string`):

Language of place names and categories, as a code: <code>en</code>, <code>es</code>, <code>de</code>, <code>pt-BR</code>.

## `country` (type: `string`):

Two-letter country code Google answers for (<code>us</code>, <code>gb</code>, <code>au</code>). Use the places' country.

## Actor input object example

```json
{
  "places": [
    "https://www.google.com/maps/place/Swish+Dental+Downtown/data=!4m2!3m1!1s0x8644b5b434046875:0x644e6eb422388b7a"
  ],
  "searchTerms": [],
  "location": "",
  "maxPlacesPerSearch": 10,
  "output": "photos",
  "maxPhotosPerPlace": 10,
  "uploadedBy": [],
  "mediaTypes": [],
  "uploadedWithinDays": 0,
  "language": "en",
  "country": "us"
}
```

# Actor output Schema

## `photos` (type: `string`):

No description

## `summary` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "places": [
        "https://www.google.com/maps/place/Swish+Dental+Downtown/data=!4m2!3m1!1s0x8644b5b434046875:0x644e6eb422388b7a"
    ],
    "maxPhotosPerPlace": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("steadyscrape/google-maps-photos-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "places": ["https://www.google.com/maps/place/Swish+Dental+Downtown/data=!4m2!3m1!1s0x8644b5b434046875:0x644e6eb422388b7a"],
    "maxPhotosPerPlace": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("steadyscrape/google-maps-photos-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "places": [
    "https://www.google.com/maps/place/Swish+Dental+Downtown/data=!4m2!3m1!1s0x8644b5b434046875:0x644e6eb422388b7a"
  ],
  "maxPhotosPerPlace": 10
}' |
apify call steadyscrape/google-maps-photos-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,steadyscrape/google-maps-photos-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/v3uYtRfWJvGCSFunN/builds/7GewLj4nV5Ysvj4Gj/openapi.json
