The Mine Works
← All posts
tutorial October 8, 2026 · 8 min read Updated October 8, 2026

Track Which Sites Rank in Google Images for Your Keywords

Use Google Images Scraper to see which domains hold the top image positions for your keywords in each country, then rerun weekly to catch movement.

Try the scraper

The actor referenced in this article. Pay only for results delivered.

View the scraper →

Image search is a ranking of its own. If you run SEO for a store, a publisher or a client, you want to know which domains own the first rows of Google Images for your keywords, where your own pages sit, and whether any of that changed since last week. Doing it by hand means scrolling a grid of about 100 tiles per keyword per country and writing domains into a sheet, and nobody keeps that up past the second week.

Try it live: Google Images Scraper: Full Size Image URLs, Sizes, Sources. Pay per result delivered. Failed and empty results are never charged.

The Google Images Scraper reads the Google Images results for each search term and returns one row per image, in Google’s order, with the domain of the page the image appears on. Group those rows by domain and you have an image ranking report.

What you get and who it is for

FieldWhy it matters for rank tracking
queryThe keyword the row belongs to
positionRank within that keyword, from 1, after duplicates are removed
pageThe Google results page the image came from
source_domainThe site that owns the ranking page, without www
source_nameThe site name Google shows, such as Vogue Arabia or Etsy
source_page_urlThe exact page that ranks
image_idGoogle’s id for the image, the same in every run, so you can follow one image over time
is_productTrue when Google shows the image as a shop listing with a price
country, languageThe market the ranking belongs to
scraped_atWhen the row was collected

This is for SEO leads at online stores who sell through product images, for publishers and design sites that get traffic from image search, and for agencies that report share of image results to clients. Shop listings are a large part of commercial grids: in our runs, 8 to 10 of the top 20 images for red sneakers carried a price.

The input

{
  "queries": ["red sneakers", "white leather sneakers", "chunky sneakers"],
  "maxResultsPerQuery": 100,
  "country": "us",
  "language": "en",
  "imageSize": "any",
  "imageType": "any",
  "timeRange": "any",
  "usageRights": "any",
  "safeSearch": false
}
  • maxResultsPerQuery: 100 is about one Google page for a common term. That is enough to count the top 20 and to spot your own site further down. The minimum is 20.
  • country and language: Google’s gl and hl settings. They apply to the whole run, so a second market needs a second run.
  • Filters stay on any. A ranking report should match what a searcher sees before touching the Tools menu.

What the output looks like

[
  {
    "query": "modern kitchen interior",
    "position": 11,
    "page": 1,
    "image_id": "JU22XoW4cpxlNM",
    "title": "What Is the 2026 Trend for Kitchens?...",
    "source_page_url": "https://www.occasionallycrafty.com/what-is-the-2026-trend-for-kitchens/",
    "source_domain": "occasionallycrafty.com",
    "source_name": "Occasionally Crafty",
    "country": "us",
    "language": "en",
    "scraped_at": "2026-10-06T09:07:36.200Z"
  },
  {
    "query": "modern kitchen interior",
    "position": 5,
    "page": 1,
    "image_id": "JU22XoW4cpxlNM",
    "title": "What Is the 2026 Trend for Kitchens?...",
    "source_page_url": "https://www.occasionallycrafty.com/what-is-the-2026-trend-for-kitchens/",
    "source_domain": "occasionallycrafty.com",
    "source_name": "Occasionally Crafty",
    "country": "us",
    "language": "en",
    "scraped_at": "2026-10-07T09:06:31.775Z"
  },
  {
    "query": "red sneakers",
    "position": 3,
    "page": 1,
    "image_id": "tHi8WUh0jLzN6M",
    "title": "Fuego Red High-top Sneaker | Size 14 Men / 15 Women...",
    "source_domain": "fuegodance.com",
    "is_product": true,
    "product_price": 155,
    "product_currency": "USD",
    "country": "us",
    "language": "en",
    "scraped_at": "2026-10-07T09:06:36.098Z"
  }
]

These rows come from two of our own runs 24 hours apart, on October 6 and October 7, 2026, for red sneakers and modern kitchen interior in the US. The same Occasionally Crafty image moved from position 11 to 5. For red sneakers, fuegodance.com held 6 of the top 20 slots on October 6 and 2 on October 7, and the number of distinct domains in the top 20 went from 13 to 17. Most of the grid held still: 15 of the top 20 image ids for red sneakers and 17 of 20 for modern kitchen interior were the same on both days.

Run it in Apify Console

  1. Open https://apify.com/themineworks/google-images-scraper and click Try for free.
  2. In Search terms, enter one keyword per line.
  3. Set Images per search term to 100, Country to us and Language to en. Leave Size, Type, Time and Usage rights on their Any settings.
  4. Click Start and watch rows arrive in the Output tab.
  5. Click Export and download CSV, JSON or Excel. In a spreadsheet, a pivot of source_domain by query, filtered to position 20 or less, gives the domain count per keyword.

Run it from Python

pip install apify-client
import csv
import glob
from collections import Counter, defaultdict
from datetime import date
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
KEYWORDS = ["red sneakers", "white leather sneakers", "chunky sneakers"]
MARKETS = [("us", "en"), ("gb", "en")]
MY_DOMAIN = "yourstore.com"
TOP = 20

rows = []
for country, language in MARKETS:
    run = client.actor("themineworks/google-images-scraper").call(run_input={
        "queries": KEYWORDS,
        "maxResultsPerQuery": 100,
        "country": country,
        "language": language,
    })
    for item in client.dataset(run["defaultDatasetId"]).iterate_items():
        if item.get("_type") == "info":
            continue  # the run summary row, never billed
        rows.append(item)

today = f"image_ranks_{date.today()}.csv"
fields = ["query", "country", "language", "position", "source_domain",
          "source_name", "image_id", "title", "source_page_url", "scraped_at"]
with open(today, "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=fields, extrasaction="ignore")
    writer.writeheader()
    writer.writerows(rows)

def best_positions(items):
    best = {}
    for r in items:
        key = (r["query"], r["country"], r["source_domain"])
        pos = int(r["position"])
        if key not in best or pos < best[key]:
            best[key] = pos
    return best

groups = defaultdict(list)
for r in rows:
    groups[(r["query"], r["country"])].append(r)

for (query, country), items in sorted(groups.items()):
    top = Counter(r["source_domain"] for r in items if r["position"] <= TOP)
    mine = [r["position"] for r in items if r["source_domain"] == MY_DOMAIN]
    print(f"{query} [{country}]: {len(items)} images, {len(top)} domains in the top {TOP}")
    for domain, n in top.most_common(5):
        print(f"    {domain}: {n} of {TOP}")
    print(f"    {MY_DOMAIN}: best position {min(mine) if mine else 'none'}")

older = sorted(p for p in glob.glob("image_ranks_*.csv") if p != today)
if older:
    with open(older[-1], encoding="utf-8") as f:
        before = best_positions(csv.DictReader(f))
    for key, pos in sorted(best_positions(rows).items()):
        old = before.get(key)
        if pos <= TOP and old != pos:
            change = "new in the list" if old is None else f"{old} to {pos}"
            print("MOVED", key, change)

The script runs once per market, saves a dated CSV, prints how many domains share the top 20 for each keyword and country, and shows the best position of your own domain. From the second run on, it compares against the previous file and lists every domain whose best position inside the top 20 changed.

Run it every week

In Apify Console, save the input as a task, one task per market. Open Schedules, create a schedule with the cron expression 0 7 * * 1 (Mondays at 07:00) and add the tasks. Every run reads Google fresh with the same input, so the weekly datasets line up row for row. The actor has no “only new” option, and rank tracking does not need one: you want the full grid each time.

To get the results out, Apify’s Google Sheets integration can append each run to a sheet, and a webhook on run success can trigger your own code or a Make, Zapier or n8n flow that posts the movers to Slack. If you prefer the Python script, run it from cron on your own machine with the same timing; it keeps the dated files it compares. Once several markets and keywords pile up, those dated files are a dataset in their own right, and it is worth deciding early how a raw run becomes a clean, stored table.

Weekly is a sensible cadence. In our two runs a day apart, three quarters or more of the top 20 stayed the same, so daily runs mostly repeat themselves.

What it costs

You pay per image delivered: $2.09 per 1,000 on the Bronze plan, $1.79 on Silver, and $1.49 on Gold, Platinum and Diamond, plus a flat $0.005 per run start.

Three keywords, 100 images each, two markets, every Monday: 600 images a week, about 2,400 a month, which is about $5.02 on Bronze plus $0.04 in start fees for eight runs. If you only care about the top 20, set Images per search term to 20 and the same schedule is 480 images a month, about $1.00 on Bronze. A term that comes back short costs less, and pages of unrelated filler, where no title holds a word of your search, are not charged.

Limits worth knowing

  • One market per run. Country, language and filters are set once for the whole run.
  • A term can come back short. Our run at 11:11 UTC on October 7, 2026 asked for 100 per term and delivered 26 for red sneakers and 11 for modern kitchen interior, two hours after a run of the same input had delivered 79 and 100. The run’s OUTPUT record gives each term a status such as no_match, no_results or blocked. Compare only runs that reached the depth you track, or every lower site will look like it dropped out.
  • Google’s depth is the ceiling. The input allows 1,000 per term, but Google stopped at 355 for red sneakers in the actor’s own test.
  • Rank, not traffic. You get who ranks and where. There is no search volume or click data in the rows.
  • The domain is the page, not the file host. source_domain is the site of the page that ranks. The file in image_url often sits on a CDN, such as i5.walmartimages.com for a walmart.com listing, so count by source_domain.
Related Actor

Explore the scraper referenced in this article: inputs, outputs, and pricing, then run it on Apify.

Frequently asked questions

Can one run track several countries? +

No. Country, language and the filters apply to every search term in a run. Start one run per market, as the Python script below does, and keep the country column when you merge the results.

How do I find where my own site ranks? +

Filter the rows on source_domain equal to your domain and take the lowest position. If your site is not in the first 100, raise maxResultsPerQuery, but expect Google to stop serving at about 300 to 400 images for a common term.

Will my own browser show the same order? +

Not always. The actor reads Google signed out, for the country and language you set, at the moment of the run. A fixed scheduled run is a steadier baseline than a check in your own browser.

Do repeated images within one search cost extra? +

No. The same image twice for one search term is never charged, and position counts images after those duplicates are removed.

Apify Store

Find a ready-made scraper for your job

Apify has over 80,000 scrapers and automations, ours included. Start free with $5 of platform credit every month.