🎉 Premium Proxies · 24-Hour Free TrialClaim Now
Use Cases

How to See Google Ads in Other Countries Without Clicking

How to see Google ads in other countries: Ads Preview first, then a clean browser behind a country proxy, no clicks, and a daily script that logs ad copy.

S SparkProxy 0 23 min read
Share

Knowing how to see Google ads in other countries matters the day a campaign targeting Germany goes live and you're sitting in Toronto, or the day a client asks what a rival is bidding on in Brazil. Searching from your own desk tells you nothing: Google places you where you are, and repeated self-searches can quietly hide your own ad from you. This guide sets up a check you can trust, from Google's own preview tool to a scripted daily log of ad copy per keyword per country.

What a country ad check has to prove

PPC managers ask two different questions, and the setup differs for each.

"Is my geo-targeted campaign actually serving there?" You own the account, so you have Google's own diagnostic tool. A live check from the target country is a second opinion, useful when the preview and reality seem to diverge or when a stakeholder wants a screenshot of the real page.

"What are competitors running abroad?" You don't own their account. Google's Ads Transparency Center shows what an advertiser has run; Google's help pages say you can "search by advertiser name or website name" and "filter their search results by serving region and date." What it won't tell you is which of your keywords trigger that ad today, in which slot, with which headline. That needs a live results page from the market. The broader research workflow, including what the ad libraries cover, is in our guide to proxies for competitor ad research, so this post stays on the mechanics of the search page itself.

Either way, a valid check proves four things at once:

Must proveHow you prove itIf you skip it
The page was served to a visitor in the target countryExit IP country logged with every captureYou record your home market by mistake
The visitor carried no historyFresh profile or fresh API session, signed outPersonalization and ad fatigue skew what appears
Nothing was clickedNo navigation to ad redirect URLs, everYou charge an advertiser and may breach Google's invalid-click policy
The page was a real results pageBlock and consent markers checked on the HTMLA challenge page gets logged as "no ads today"

Most broken checks fail on the last row. "No ads found" is the most common false result in this work, and it usually means "no results page found."

Ads Preview first: the free, impression-free check

If the campaign is yours, open Google's Ad Preview and Diagnosis tool before you touch a proxy. In Google Ads it sits under Tools, in the Troubleshooting section. Google's help page describes it as a tool that "helps you understand why your ad or ad asset might not be appearing" and says using it "is preferable to conducting a Google search yourself" because it "avoids accumulating ad impressions and impacting your performance statistics."

Google's Search Ads 360 help for the same tool says you enter a search term "and other criteria like language and location," so you can point it at Munich or São Paulo without leaving your desk. Run every keyword you plan to monitor through it once, per target country, and note which ones return a preview and which return a diagnosis such as a budget or bid issue.

Know its limits before you treat it as ground truth. Google's own wording: "the tool may not reflect all campaign activity," "the search results returned by this tool may change over time," and "Keywords with limited search history may not trigger preview simulations." The SA360 page adds that a preview "may no longer appear in the tool when your campaign reaches its daily budget." So an empty preview at 6 p.m. local time is often a spent budget, not a targeting fault.

Ads Preview also can't show competitors in a way you can log over time, and it isn't evidence a client can verify independently. That's where the live view comes in.

Free trial

Scraping at scale? Skip the blocks.

Fast, unblockable datacentre proxies with unlimited bandwidth.

The no-click rule, and why impressions count too

Never click a sponsored result during a country check. Not yours, not a competitor's, not "just once to see the landing page."

Google's invalid-clicks page defines them as "clicks on your ads that don't represent a genuine interest in your business," and lists "Clicks from automated tools, bots, or other deceptive software" and manual "Clicks that increase your ad costs or inflate publisher profits." A monitoring script that follows an ad link fits the first description. A person clicking a rival's ad to peek fits the second. If you need the landing page, take the final URL from the ad's visible display URL or from the Transparency Center, and fetch that directly.

Impressions are the quieter problem. Google's "Check where your ad appears" help page puts it plainly: "By performing searches that trigger your ad, you'll accumulate impressions without clicks, which can lower your clickthrough rate and prevent your ad from appearing as often as it should." It goes further: "if you repeatedly look for your own ad using Google search but never click on it, you might stop seeing it entirely," because "Google's system stops showing you ads that it thinks you aren't interested in."

That last sentence is why the clean profile in the next section isn't optional. A PPC manager who self-searches from the same browser every morning is training Google to hide the ad from exactly that browser, then filing a ticket because the ad "disappeared." It also sets a volume rule for automation: every live capture is a real search that can register impressions for the advertisers on the page. Capture each keyword once per day per country, not every ten minutes.

A clean browser profile behind a country exit

For a person looking at the page, three things change together: the IP, the browser state, and the language. Changing only the IP leaves cookies and your Google account pointing home.

Google's location-targeting help says targeting relies on "a variety of signals, including users' settings, devices, and behavior on our platform," and calls it "best effort." A signed-out, cookie-free browser strips away the settings and behavior signals you carry, which leaves the network location doing most of the work. That's the part a country proxy supplies.

On Windows, a separate Chrome profile with a German exit looks like this:

:: Throwaway profile per market, German exit on the Sticky Sessions port
"C:\Program Files\Google\Chrome\Application\chrome.exe" ^
  --user-data-dir="C:\ad-checks\profile-de" ^
  --proxy-server="http://gateway.sparkproxy.io:11002" ^
  --lang=de-DE ^
  --no-first-run ^
  "https://www.google.com/search?q=kfz+versicherung+vergleich&hl=de"

Chrome will ask for the proxy credentials. Country is chosen in the username: the sample on sparkproxy.io shows the form user-country-us, so Germany is user-country-de and France is user-country-fr. Whitelisting your office IP in the dashboard removes the prompt. Step-by-step proxy setup for Chrome and Firefox, including PAC files, lives in our Chrome and Firefox proxy guide.

Why port 11002? That's Sticky Sessions, which holds one exit IP for 5 minutes. A person scrolling, changing the query and paging through results should look like one visitor; Random Rotation on port 11000 would hand each request a new IP mid-session. Our explainer on sticky session proxies covers when that matters. For scripts, the opposite holds and each capture should be a fresh visitor.

A short pre-flight inside the proxied window, every time:

  1. Load an IP-lookup page and confirm the country before searching.
  2. Stay signed out; never sign in to Google in a check profile.
  3. Answer the cookie-consent screen the way a local visitor would. In the EU it is part of what real users see.
  4. Run the keyword, screenshot the full page, and write down the time in the market's own time zone.
  5. Close the profile, and delete the profile folder weekly so it never accumulates history.

Keep one profile folder per market. Mixing Germany and France in one profile carries consent cookies and language settings across, and the French page you screenshot may carry German preferences.

Reading the sponsored block correctly

Google changed the ad presentation in October 2025. Its post of 13 October 2025 introduced a grouped "Sponsored results" label with a "Hide sponsored results" control, said "you'll still never see more than four text ads in a grouping," and stated the updates were "rolling out globally on desktop and mobile." Shopping units carry "Sponsored products."

Three consequences for a country check:

  • The label is translated. A German or Brazilian page shows the label in the interface language, so a script that searches the HTML for the English word "Sponsored" will report zero ads on every non-English page. Detect ads by page structure, and save raw HTML so you can confirm the selector when Google changes markup.
  • A grouping holds at most four text ads. If your parser returns six from one block, it is double-counting nested elements.
  • Top and bottom blocks are different placements. Record which block an ad came from. An ad that slides from the top block to the bottom one in a market is a bid or Quality Score story, not a missing ad.

What to record per ad: block (top or bottom), position within the block, advertiser display domain, headline, description, and the visible display URL path. Sitelinks and other assets are worth a separate column, since they change more often than headlines.

The daily capture pipeline, end to end

The scripted version replaces the person, and keeps every rule above.

keywords.csv (keyword, market, language)
        |
        v
scheduler (cron, 07:00 in each market)
        |
        v
SparkProxy Scraping API  -- country_code=DE, render_js=true, json_response=true
        |   (or Playwright through gateway.sparkproxy.io with user-country-de)
        v
decode base64 body -> block/consent check -> parse sponsored blocks
        |
        v
SQLite: captures (one row per run) + ads (one row per ad) + hash of ad copy
        |
        v
daily diff report: new advertisers, changed headlines, missing own ads

Proxy decisions for the scripted version:

DecisionChoiceReason
RotationRandom Rotation (or the API's default per-request exit)Each keyword should be searched by a fresh visitor with no ad fatigue
CountryOne code per market, matching the query languageA French query from a German exit is neither market
Concurrency1 to 2 requests per market at a timeSearches are low volume; bursts invite challenges and add nothing
TimingSame local hour every day per marketBudgets and day-parting change the ad block through the day
DevicesDesktop by default; mobile as a separate runMobile and desktop are separate auctions with separate layouts

The Scraping API is the simpler path for HTML. If you need mobile emulation or full-page screenshots under your own control, run Playwright through the gateway, shown later.

Capture script: one keyword, one country, one row per ad

This uses the SparkProxy Scraping API at https://scrape.sparkproxy.io/api/v1 with the key in the X-API-Key header. render_js defaults to true, which matters because the results page needs a real browser. json_response=true wraps the result in an envelope with status_code, credits_used and body, and the body arrives Base64-encoded. Decode it before looking for anything. (The format parameter is unrelated; it converts content to Markdown, PDF and so on, and format=json does not give you this envelope.)

import base64
import os
import time
from urllib.parse import urlencode

import requests

API = "https://scrape.sparkproxy.io/api/v1"
KEY = os.environ["SPARKPROXY_API_KEY"]

BLOCK_MARKERS = ("/sorry/index", "captcha-form", "g-recaptcha")
CONSENT_MARKERS = ("consent.google.", "before you continue")


def google_search_url(keyword, lang):
    return "https://www.google.com/search?" + urlencode({"q": keyword, "hl": lang})


def fetch_results_page(keyword, country, lang):
    params = {
        "url": google_search_url(keyword, lang),
        "country_code": country,
        "json_response": "true",
    }
    resp = requests.get(API, params=params, headers={"X-API-Key": KEY}, timeout=150)
    resp.raise_for_status()
    env = resp.json()
    html = base64.b64decode(env["body"]).decode("utf-8", errors="replace")
    return env.get("status_code"), env.get("credits_used", 0), html


def classify(html):
    lowered = html.lower()
    if any(m in lowered for m in BLOCK_MARKERS):
        return "blocked"
    if any(m in lowered for m in CONSENT_MARKERS) and "id=\"search\"" not in lowered:
        return "consent"
    if "id=\"search\"" not in lowered and "id=\"rso\"" not in lowered:
        return "not_a_results_page"
    return "ok"

The markers are heuristics, not a Google contract: /sorry/index is the path Google's challenge page has used, and the results container ids are what we check for in saved pages. Re-confirm all of them against a stored capture whenever counts drop to zero across every market at once, because that pattern means markup changed, not that every advertiser paused.

Parsing the sponsored blocks needs an HTML parser. This version uses BeautifulSoup:

from urllib.parse import urlparse

from bs4 import BeautifulSoup

# Assumed containers for the top and bottom ad blocks. Verify on a saved page.
AD_BLOCKS = {"top": "#tads", "bottom": "#bottomads"}


def parse_ads(html):
    soup = BeautifulSoup(html, "html.parser")
    ads = []
    for block_name, selector in AD_BLOCKS.items():
        block = soup.select_one(selector)
        if block is None:
            continue
        position = 0
        for link in block.select("a[href]"):
            heading = link.select_one("[role=heading], h3")
            if heading is None:
                continue
            position += 1
            card = link.find_parent("div", attrs={"data-text-ad": True}) or link.parent
            text = " ".join(card.get_text(" ", strip=True).split())
            title = heading.get_text(" ", strip=True)
            ads.append({
                "block": block_name,
                "position": position,
                "headline": title,
                "display_domain": urlparse(link.get("data-pcu", "") or "").netloc,
                "snippet": text.replace(title, "", 1).strip()[:500],
            })
            if position >= 4:  # Google caps a grouping at four text ads
                break
    return ads

Note what the code never does: it never requests the href of an ad. The link is read as text and discarded. The data-pcu attribute is an assumption about where Google exposes the display URL; if it is missing in your captures, take the domain from the visible display-URL text instead. For organic-result parsing and the general structure of a results page, see how to scrape Google search results.

Storing ad copy so changes stand out

Two tables are enough. One row per capture records whether the page was valid; one row per ad holds the copy. A hash of the copy makes "what changed since yesterday" a single query.

import hashlib
import sqlite3
from datetime import datetime, timezone

SCHEMA = """
CREATE TABLE IF NOT EXISTS captures (
  id INTEGER PRIMARY KEY, captured_at TEXT, market TEXT, keyword TEXT,
  verdict TEXT, upstream_status INTEGER, credits INTEGER, ad_count INTEGER
);
CREATE TABLE IF NOT EXISTS ads (
  capture_id INTEGER, block TEXT, position INTEGER, display_domain TEXT,
  headline TEXT, snippet TEXT, copy_hash TEXT
);
"""


def save_capture(db, market, keyword, verdict, status, credits, ads):
    cur = db.execute(
        "INSERT INTO captures (captured_at, market, keyword, verdict, upstream_status,"
        " credits, ad_count) VALUES (?, ?, ?, ?, ?, ?, ?)",
        (datetime.now(timezone.utc).isoformat(timespec="seconds"), market, keyword,
         verdict, status, credits, len(ads)),
    )
    for ad in ads:
        digest = hashlib.sha256((ad["headline"] + "|" + ad["snippet"]).encode()).hexdigest()
        db.execute(
            "INSERT INTO ads VALUES (?, ?, ?, ?, ?, ?, ?)",
            (cur.lastrowid, ad["block"], ad["position"], ad["display_domain"],
             ad["headline"], ad["snippet"], digest[:16]),
        )
    db.commit()


def open_db(path="ad_checks.sqlite"):
    db = sqlite3.connect(path)
    db.executescript(SCHEMA)
    return db

The rule that keeps the data honest: a capture with verdict blocked, consent or not_a_results_page gets a captures row and zero ads rows, and reports never read it as "no ads." Only ok captures with ad_count = 0 mean the market showed no ads for that keyword.

A useful daily query lists copy that appeared today and not yesterday, per market:

SELECT c.market, c.keyword, a.display_domain, a.headline
FROM ads a JOIN captures c ON c.id = a.capture_id
WHERE date(c.captured_at) = date('now') AND c.verdict = 'ok'
  AND a.copy_hash NOT IN (
    SELECT a2.copy_hash FROM ads a2 JOIN captures c2 ON c2.id = a2.capture_id
    WHERE date(c2.captured_at) = date('now', '-1 day')
      AND c2.market = c.market AND c2.keyword = c.keyword);

For your own campaigns, flip it: alert when your domain is absent from an ok capture in a market where Ads Preview said it should serve.

Scheduling, failure handling and retries

The runner ties the pieces together, retries only on failures that a fresh exit can fix, and spaces requests out:

import csv
import random
import sys


def run(keywords_csv="keywords.csv", attempts=3):
    db = open_db()
    with open(keywords_csv, newline="", encoding="utf-8") as f:
        jobs = list(csv.DictReader(f))  # columns: keyword, market, lang
    for job in jobs:
        verdict, status, spent, ads = "error", None, 0, []
        for n in range(attempts):
            try:
                status, credits, html = fetch_results_page(job["keyword"], job["market"], job["lang"])
                spent += credits
                verdict = classify(html)
                if verdict == "ok":
                    ads = parse_ads(html)
                    break
            except requests.RequestException:
                verdict = "error"
            if verdict == "consent":
                break  # a new IP will not remove the consent screen
            time.sleep(20 * (n + 1) + random.uniform(0, 10))
        save_capture(db, job["market"], job["keyword"], verdict, status, spent, ads)
        time.sleep(random.uniform(4, 9))


if __name__ == "__main__":
    run(sys.argv[1] if len(sys.argv) > 1 else "keywords.csv")

Schedule one run per market at that market's morning, so the comparison is like for like:

# Germany and France at 07:00 CET (06:00 UTC), Brazil at 07:00 BRT (10:00 UTC)
0 6 * * *  cd /opt/ad-checks && python3 capture.py keywords_eu.csv >> logs/eu.log 2>&1
0 10 * * * cd /opt/ad-checks && python3 capture.py keywords_br.csv >> logs/br.log 2>&1

Which failures a proxy can fix, and which it can't:

What you seeCauseProxy fixes it?Fix
Verdict `blocked`Challenge page for that exitYesRetry after a pause on a new exit; try `proxy_type=premium` if a market keeps failing
Exit IP in the wrong countryWrong username suffix or `country_code`YesFix the country code, log the exit country
API returns 429Plan rate or concurrency limitNoLower concurrency, honor `retry_after_seconds`
API returns 530 or 500Scrape failed upstreamPartlyRetry later; the docs say credits are refunded for these
Verdict `consent`EU consent interstitialNoHandle consent in a browser session (Playwright route below)
`ok` page, your ad absent, Ads Preview shows itBudget spent, day-parting, or ad fatigue in a reused profileNoCheck budget pacing and schedule; never reuse profiles
`ok` page, zero ads in every marketGoogle markup changedNoRe-check the selectors against saved HTML

The Playwright route through the gateway suits mobile runs and consent handling. It also adds a hard guard: any request to an ad-click redirect is aborted before it leaves the browser.

import os
from urllib.parse import quote_plus

from playwright.sync_api import sync_playwright

CLICK_GUARDS = ("googleadservices.com", "/aclk", "/pagead/aclk")


def guard(route):
    if any(g in route.request.url for g in CLICK_GUARDS):
        route.abort()
    else:
        route.continue_()


def capture_mobile(keyword, country, lang, out_png):
    proxy = {
        "server": "http://gateway.sparkproxy.io:11000",
        "username": os.environ["SPARKPROXY_USER"] + "-country-" + country.lower(),
        "password": os.environ["SPARKPROXY_PASS"],
    }
    with sync_playwright() as p:
        browser = p.chromium.launch(proxy=proxy)
        context = browser.new_context(**p.devices["iPhone 13"], locale=lang)
        page = context.new_page()
        page.route("**/*", guard)
        page.goto("https://www.google.com/search?hl=" + lang + "&q=" + quote_plus(keyword),
                  wait_until="domcontentloaded", timeout=60000)
        page.wait_for_timeout(3000)
        page.screenshot(path=out_png, full_page=True)
        html = page.content()
        browser.close()
    return html

Feed the returned HTML through the same classify and parse_ads functions so both routes land in one table.

What a daily run costs

On the Scraping API, a rendered request costs 5 credits and country_code adds 5, so each capture is 10 credits. A screenshot through the API adds another 5. If a market forces proxy_type=premium, the docs list 25 credits with JavaScript, plus 5 for the country. New accounts get 1,000 free credits.

Plan ladder as listed on sparkproxy.io/scraping-api, read 30 October 2026, prices in US dollars per month excluding VAT: Starter $49 (250,000 credits, 50 concurrent), Growth $99 (1,000,000 credits, 100 concurrent), Pro $249 (3,000,000 credits), Scale $599 (8,000,000 credits).

Worked examples with assumed workloads and 25% headroom for retries:

Assumed workloadCaptures per monthCredits (10 each, +25%)Fits
10 keywords, 2 countries, daily6007,500Starter
40 keywords, 4 countries, daily4,80060,000Starter
150 keywords, 6 countries, daily, desktop and mobile54,000675,000Growth
Same as above, all on `proxy_type=premium` (30 each)54,0002,025,000Pro

These are arithmetic on assumed inputs, not measured spend. Failed 500 and 530 requests are refunded per the docs, which pulls real spend down.

On the gateway route, proxy plans are packs "Starting from $75/month" with unlimited bandwidth, so full-page mobile screenshots don't raise the bill; you pay for the plan and the server running the browsers. Term discounts are 5%, 10% and 15% for 3, 6 and 12 months, and a 24-hour free trial needs no credit card, enough to run your keyword list through every market once before committing.

When the live view and Ads Preview disagree

This is the case that brings most PPC managers here. Work through it in order, cheapest first:

  1. Time of day. Ads Preview at your 4 p.m. may be the market's 10 p.m., after the daily budget ran out. Compare captures taken at the same local hour.
  2. Location option. Google Ads offers "Presence or Interest" and "Presence" targeting. Google's help describes the broad option as reaching people who are in your locations or "have shown an interest in" them. If the campaign is set to Presence, only a visitor placed in the country should see it, which a clean country exit reproduces; Ads Preview is set by the location you type.
  3. Ad fatigue in a reused profile. If the human check used a profile that searched the keyword yesterday, Google may be suppressing the ad for it, per the help text quoted earlier.
  4. Language. A campaign targeting German speakers will not show to a browser whose language says English. Match hl and the browser locale to the campaign's language setting.
  5. A real serving gap. If all four check out and three days of morning captures miss the ad while Ads Preview shows it, you have evidence worth escalating with screenshots and timestamps.

The broader discipline of checking your own delivery, beyond search, is covered in how brands use proxies for ad verification. For running this across hundreds of keywords and several engines, our notes on proxies for SERP scraping at scale cover the throughput side.

One limit no setup removes: SparkProxy sells datacenter IPs, Google doesn't publish how it treats datacenter ranges, and we haven't measured a challenge rate for our exits on Google, so we quote none. The verdict column is how you find out for your own keyword set.

Frequently asked questions

FAQ

For your own campaigns, use the Ad Preview and Diagnosis tool in Google Ads and enter the target location and language. For competitors, the Ads Transparency Center filters an advertiser's ads by serving region. To see the live results page with its ad block as a local visitor gets it, you need an exit IP in that country.

It can. Google's help says self-searches "accumulate impressions without clicks," which can lower your clickthrough rate, and that repeatedly searching without clicking means "you might stop seeing it entirely." Use Ads Preview, which avoids accumulating impressions.

No. Google defines invalid clicks to include manual "Clicks that increase your ad costs" and clicks from automated tools. Fetch the landing page directly from the display URL or the Transparency Center instead of through the ad.

The usual causes, in order: the daily budget was spent by the time you checked, the location option differs from what you tested, the browser profile had ad fatigue, or the language did not match. Compare captures taken at the same local hour in a fresh profile before calling it a serving problem.

Use Sticky Sessions, which hold one IP for 5 minutes, when a person is browsing and paging in one window. Use Random Rotation for scheduled captures so every keyword is searched by a fresh visitor.

A rendered capture with country_code costs 10 credits. Forty keywords in four countries daily is about 60,000 credits a month with retry headroom, inside the $49 Starter plan as listed on 30 October 2026, and a first test fits in the 1,000 free credits.

Special Discount · 20% off

Get 20% off your first month

Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.

Save up to 15% more on quarterly, half-yearly and yearly plans

Claim Discount

About the Author

This guide was written by the SparkProxy Technical Team, which runs the SparkProxy proxy network (1M+ premium IPs in 80+ countries, HTTP/HTTPS/SOCKS5, Random Rotation and Sticky Sessions) and the SparkProxy Scraping API. Google behavior is quoted from Google's own pages, read on 30 October 2026: "About the Ad preview and diagnosis tool" and "Check where your ad appears on Google search results" (Google Ads Help), "Ad Preview and Diagnosis tool" (Search Ads 360 Help), "Invalid clicks: Definition," "Target ads to geographic locations," the Ads Transparency Center description in My Ad Center Help, and the Google blog post of 13 October 2025 on sponsored results. HTML selectors in the code are assumptions to verify against your own saved pages. Corrections: support@sparkproxy.io.

Keep reading

Related articles