Ad verification fails quietly. You open the campaign URL from the office, the creative renders, the landing page loads, the spreadsheet says "live in all markets". Then a customer in another city sends a screenshot of a competitor's ad in the slot you paid for, or a blank page, or a consent wall that never offers a way through. The check passed because it was never a check: one IP, one city, one connection type, one cookie state.
What you are actually verifying is not the creative. It is the delivery decision — and that decision is made from three signals you can only reproduce with the right exit.
Three layers decide what renders
| Layer | What you control | What the ad stack sees | Get it wrong and... |
|---|---|---|---|
| Network | Country, city, ASN type | Geo of the exit, ISP vs hosting, connection type | Fallback creative, no-fill, or a page that pretends the market is unsupported |
| Device | Viewport, UA, is_mobile, timezone, locale | Device class, OS, browser, language | Mobile-only formats invisible; layout and creative variant differ |
| Audience state | Cookies, consent choice, logged-out vs logged-in | New vs returning, consent signal, interest buckets | You measure the first-visit page and call it the customer experience |
The first layer is the one teams handle. The second is where most budgets leak: programmatic inventory is priced and allocated by metro, so a country-level check cannot see a campaign that under-delivers in one city and over-delivers in another. The third layer is the one that produces the "it works for me" argument — a fresh profile with no consent cookie sees a different page from a returning visitor, and both are legitimate.
Match the exit to the verification task
| Verification task | Exit needed | Session | Most common mistake |
|---|---|---|---|
| Creative / copy check on your own campaign | Residential, any city in the buy | Sticky 5 min | Screenshotting a desktop page for a mobile placement |
| Geo delivery by city or DMA | Residential, one exit per city | Sticky 20 min per city | Sampling three cities and generalising to twenty |
| Landing page + redirect chain integrity | Residential, same city as the ad | Sticky for the full chain | Rotating between click and landing page |
| Consent banner and CMP behaviour | Residential, EU market | Three passes: accept / reject / undecided | Only ever testing the accept path |
| Publisher placement and adjacency | Residential or datacenter, high volume | Rotating | Judging placement from one impression |
| Mobile app-install and carrier formats | Mobile ASN | Sticky 10 min | Using a fixed-line residential exit and reporting a false negative |
Note the pattern: the more the task depends on being treated as a person, the more it needs a residential exit and a stable session. Only the first row is safe on datacenter, and even there the platform may serve a stripped page.
Keep one exit for the whole chain
An ad check is a sequence, not a request. Search or feed → impression → click → redirect → landing page → CTA. Frequency capping, A/B assignment and geo-redirects are all session-scoped. Rotate in the middle and every downstream measurement is taken against a different user than the one who saw the ad. In practice that produces the two most confusing reports in the discipline: "creative matched but landing page 404" and "ad served in a city we did not target".
# One verification pass = one sticky exit for 20 minutes # Gateway form: country-city-session-ttl PROXY = "http://user-country-de-city-berlin-session-8f21a:pass@gate.iplight.net:8000" # session id stays fixed; ttl keeps the same exit alive across requests # Playwright: drive the real chain, capture what rendered from playwright.sync_api import sync_playwright with sync_playwright() as p: br = p.chromium.launch(proxy={"server": "http://gate.iplight.net:8000", "username": "user-country-de-city-berlin-session-8f21a", "password": PASS}) ctx = br.new_context(locale="de-DE", timezone_id="Europe/Berlin", viewport={"width": 390, "height": 844}, is_mobile=True) pg = ctx.new_page() pg.goto(CAMPAIGN_URL, wait_until="networkidle", timeout=45000) # 1. did we leave the domain (redirect chain changed the market)? print("final", pg.url) # 2. does the geo layer agree with the exit? print("geo", pg.evaluate("fetch('/cdn-cgi/trace').then(r=>r.text())")) # 3. evidence, one per city per day pg.screenshot(path="de-berlin-2026-11-03.png", full_page=True) br.close()
Two details in that snippet do more work than the proxy itself: locale/timezone_id
matching the exit, and is_mobile. A German exit with an English locale and a desktop viewport gets a
different layout and often a different creative, and you will chase a "missing ad" that was a viewport mismatch.
Measure the things that can actually be wrong
| Metric | How to compute it | Healthy |
|---|---|---|
| Geo match rate | Passes where the page's own geo == the exit's city ÷ total passes | > 98% |
| Creative match rate | Passes showing the expected creative variant ÷ total | > 95% |
| Chain integrity | Passes reaching the intended final URL ÷ passes that clicked | 100% |
| Landing page status | Share of passes returning 200 at the final URL | > 99% |
| Consent variants covered | Distinct banner outcomes seen ÷ 3 | 3 of 3 |
| Cost per verified placement | Monthly proxy + storage spend ÷ placements verified | Report, do not chase |
Geo mismatch is the metric that pays for the programme. If the exit says Berlin and the page's own geo layer says Amsterdam, your targeting is leaking spend into a market you did not buy — and that is a budget conversation, not a technical one. Track it per city, weekly, and put the number in the same deck as delivery.
What a programme costs
| Item | Assumption | Value |
|---|---|---|
| Placements | Own campaigns + partner placements | 200 |
| Cities | Top metros in the buy | 8 |
| Passes per month | Twice weekly | 2 |
| Page loads | 200 × 8 × 2 | 3,200 |
| Traffic per load | Images and fonts blocked | ≈ 700 KB |
| Monthly traffic | 3,200 × 700 KB | ≈ 2.2 GB |
| Monthly cost at $1/GB | Traffic × unit price | ≈ $2.20 |
The arithmetic: 200 placements × 8 cities × 2 passes a month = 3,200 page loads, at an average of 700 KB with images blocked, is about 2.2 GB a month, so roughly $2.20 at $1/GB. Even at 10x that volume the proxy line stays smaller than the screenshot storage and the engineer's time. Treat traffic cost as noise and spend the effort on coverage: more cities, more device profiles, more consent states.
Six failure modes worth pre-empting
- Country-level checks only. Buys and delivery are metro-level; a national pass reports healthy while a city is dark.
- Rotating mid-chain. Attribution, frequency caps and A/B buckets all reset. Sticky, or the number is meaningless.
- Desktop-only viewport. Most social and app-install inventory is served to mobile; a desktop pass cannot see it.
- Fresh cookie jar every run. You verify the first-visit experience forever and never the returning one, which is the one most customers have.
- No consent-state variants. Accept, reject and undecided produce three different pages in the EU; two of them are what most visitors see.
- Verifying competitors by clicking. That is click fraud, not verification, and it costs the other side real money.
Frequently asked questions
Why does ad verification need residential proxies rather than datacenter ones?
Because the ad stack treats datacenter traffic differently. Google and Meta both suppress or de-prioritise ad delivery to known hosting ASNs, and many publishers' consent and geo layers serve a fallback page instead of the real one. A datacenter exit therefore does not show you the ad a customer sees — it shows you the page the system serves when it does not believe you are a customer. Residential exits come from consumer ISPs, so you land in the same delivery buckets as the audience you are checking.
How granular does the location need to be?
City, not country. Most programmatic buys are planned at DMA or city level and priced on local inventory, so a national check reports "live" for a campaign that never serves in the two cities you actually bought. Run one exit per city in the plan, at minimum the top five by spend. Where the buy is postcode- or radius-targeted, add one pin per postcode cluster.
Should the session rotate or stay sticky?
Sticky for the whole verification pass. A single check is a chain: search or feed, then the ad, then a click, then two or three redirects, then the landing page. If the exit changes mid-chain, frequency caps reset, A/B buckets change and the redirect can resolve to a different geo — you end up measuring three different users and calling it one. Keep one exit for 10–30 minutes, then move on.
Do mobile creatives need a mobile connection?
Yes. Carrier-only formats — app-install ads, in-app interstitials, carrier billing offers — are targeted at mobile networks and often will not serve to a fixed-line residential IP at all. If the plan includes mobile placements, verify them from mobile ASNs; a residential ISP exit will report a false negative and you will escalate a problem that does not exist.
How much traffic does a monthly verification programme use?
Less than most teams assume. A text-and-image page pass is roughly 300–600 KB; a video or heavy retail page is several megabytes. Two hundred placements across eight cities, once a week, lands in the 5–15 GB a month range for image-heavy checks and well under 3 GB if you block images and fonts. Screenshot storage costs more than the proxy traffic.
Can I verify ads without breaching the platform's terms?
Checking how your own campaigns render is standard brand-protection work and is what the ad platforms' own transparency tools are for. What is not acceptable: clicking your competitors' ads to burn their budget, generating fake impressions or conversions, or scraping logged-in user data. Keep verification read-only, rate-limited, and never click a paid ad you are not prepared to pay for.
Run one city before you run twenty
Residential exits from $1/GB billed by traffic, city-level targeting, sticky sessions up to 30 minutes. Verify a single market end to end before you build the matrix.
Create account