Connected TV is now where much of video ad spend goes, and it is the hardest place to check what you paid for. A campaign bought for viewers in Texas can play in New York, on an app you never chose, or not play at all, and the report still shows delivered impressions. Ad verification on CTV means watching the ad breaks the way a real viewer in each region would.
This guide covers why CTV is hard to verify, which checks matter, and a practical way to see the ads that really play in each region.
Why CTV is harder to verify
On a web page, a verification vendor runs a script next to the ad and reads what happens. On a TV, that mostly does not work:
- Server-side ad insertion (SSAI). Many streams stitch ads into the video on a server before it reaches the TV. The ad request comes from that server, which passes on the viewer’s IP address and device details in headers. If a fraudster fakes that server, the “viewer” can be invented.
- Limited measurement in apps. TV apps run on many operating systems, and not all of them support the measurement SDKs common on web and mobile, so viewability and device checks are patchy.
- No cookies. CTV relies on a device advertising ID and the household IP. Both can be reset or spoofed.
- Region is set by IP. Most streaming services decide which ads a viewer gets, and often which content, from the location of the household IP.
- Long, layered supply chains. An impression can pass through the app, a supply platform and several resellers before you buy it.
The result: CTV reports describe what the ad server was told, not necessarily what played on a screen.
What to check
A useful CTV verification routine covers five things:
- Geography. Did the ad play only in the regions you bought? Check from inside each target region, and from outside it to catch spill.
- Inventory. Did it play on the apps and channels you were sold? Compare the app with the publisher’s
app-ads.txtand the seller list you agreed. - Creative. Is the right version running in each market, including language, offer and legal text?
- Delivery quality. Did the full ad play, in a clean break, without a black slate, a cut or a repeat of the same spot?
- Context. What content ran around the ad? Brand safety matters as much on streaming as on the web.
Delivery logs answer none of these on their own. You need observations from the viewer’s side.
How to check streaming ads by region
Many ad-supported streaming services also run in a web browser, and their web players request ads much like the TV apps do. That makes a browser behind a local IP a practical observation point.
1. Build a region and channel plan
List the regions in your campaign, the services and channels you bought, and the times your spots should run. Add one or two regions you did not buy, to catch delivery outside the plan.
2. Enter each region through a residential IP
Streaming services judge location from the IP and often refuse or reshape datacenter traffic. Use residential proxies targeted at the city you need, and keep one IP for a whole viewing session with a sticky session ID, so the service sees one household.
customer-USERNAME-country-us-city-dallas-sid-dal-ctv-01-ttl-1800:PASSWORD
3. Play the stream and record the ad breaks
Open the web player in a real browser, start a stream, and capture two things: the ad requests and responses the player makes (often VAST or VMAP XML), and the video of each break. Record per ad: creative ID, advertiser, duration, position in the break, region, time and the content around it.
A minimal capture with Playwright looks like this:
from playwright.sync_api import sync_playwright
PROXY = {
"server": "http://p.shifter.io:443",
"username": "customer-USERNAME-country-us-city-dallas-sid-dal-ctv-01-ttl-1800",
"password": "PASSWORD",
}
def capture_ad_calls(stream_url, seconds=600):
seen = []
with sync_playwright() as p:
browser = p.chromium.launch(proxy=PROXY, headless=False)
page = browser.new_page()
def on_response(resp):
ctype = resp.headers.get("content-type", "")
if "xml" in ctype and ("vast" in resp.url.lower() or "vmap" in resp.url.lower()):
seen.append({"url": resp.url, "status": resp.status, "body": resp.text()})
page.on("response", on_response)
page.goto(stream_url, wait_until="domcontentloaded")
page.wait_for_timeout(seconds * 1000)
browser.close()
return seen
The URL filter is a starting point; each service names its ad calls differently, so look at the network tab once and adjust it. With server-side insertion the ad is already inside the video, and there may be no VAST call to capture. Then the screen recording of the break is your evidence.
4. Compare with the plan
For each region and service, match what played against what was bought. Flag ads seen outside the bought regions, missing ads in bought regions, wrong creative versions, and breaks with slates or repeats.
5. Spot-check native apps
For a native TV app, put a test device on a network routed through the proxy and repeat the session. Treat this as a spot check: apps can use their own ways to locate a viewer.
Fraud signs worth watching
Viewer-side checks also surface the patterns behind most CTV fraud:
- Delivered impressions in regions where no ad ever appears in your own sessions.
- Apps on the invoice that you cannot find, or that do not list the seller in their
app-ads.txt. - Impossible device mixes, such as heavy “TV” traffic from a datacenter network.
- Breaks longer than the content allows, a sign of stuffed ad pods.
For the detection side, our guide to detecting ad fraud in real time covers the signals in more depth.
Practical tips
- Rotate households, not just cities. Use a new session ID per viewing session so one IP does not stand for a whole market.
- Watch at the right hours. Many campaigns are dayparted, so an empty break at 3 a.m. proves nothing.
- Match the browser to the region. Time zone and language should fit the IP, or the service may treat the visit as unusual.
- Keep the evidence. Save the recording, the ad response and the IP location for every flagged break, so a make-good claim stands up.
- Use the same method for display. The approach is the same one used for catching cloaked and geo-targeted ads on the web.
The bottom line
CTV reports tell you what the ad server was told. To know what actually played, watch the breaks from inside each region: a residential IP in the target city, one household per session, a recording of every break, and a comparison against the plan. Do it on a schedule across the regions you buy, and spill, missing delivery and wrong creatives show up before the campaign ends.