CTV Testing and Troubleshooting
The tag fails soft. A malformed piece of a request is dropped rather than returned as an error, and a no-fill looks like a normal empty response. That keeps players simple, but a broken integration can look fine and still earn less. Use this page to check yours.
The three response shapes
curl -isL "https://g.ezoic.net/ezoic/videoad/vast?d=example.com&devicetype=3&plcmt=1&v=3&w=1920&h=1080&cb=$RANDOM"
-L follows redirects. -i shows the headers.
| You see | Meaning |
|---|---|
302, then VAST XML with an <Ad> |
Filled through a redirect. This is the normal path. Players follow it automatically. |
200 with VAST XML containing one or more <Ad> elements |
Filled inline. Extra <Ad> elements are same-request fallbacks. A player plays the first that renders. |
200 with an empty VAST document |
No fill for this request. This is not an error. |
| Anything else, such as a 4xx, a 5xx or HTML | The request URL is malformed. Check for an unencoded value that breaks the query string. |
No-fill is expected on test requests, especially before your placement is active and your app identity is validated. A correctly shaped empty response proves the request mechanics work.
The empty response echoes the v you sent. v=3 returns version="3.0". Use this to confirm your version arrived.
Check 1: parameters arrive
Capture a real request from a real session, from the device network log, the stitcher session log, or browser developer tools, and inspect the query string:
- Every macro expanded. No literal
[IFA],{{BUNDLE}}or%%US_PRIVACY%%appears anywhere in the URL. An unexpanded macro is dropped. This is the most common integration bug. -
ifais a real device ID: not empty, and not all zeros unless the viewer limited tracking.ifa_typematches the platform. - Server-side callers:
ipanduaare the viewer's. Check the IP against a geo lookup. If it resolves to your data centre, it is wrong. -
bundleis the real store identifier. On Roku store builds this is the numeric Channel ID. Sideloaded builds reportdev, which is fine in development and wrong in production. - Content fields carry real values, URL-encoded.
-
vmatches your player. Use3on Roku.devicetype=3is present. -
adunitis your normal video ad unit name.
Unexpanded macros
A value that is entirely a macro literal is dropped before the request is processed. The patterns are [...], {{...}}, {...}, %%...%%, ${...}, $$...$$ and __...__. The literal text null and undefined is also dropped, in any case. A value is dropped only when it is entirely a macro literal, so content_title=Episode [Pilot] survives.
Dropped keys are logged server-side. A player integration that writes undefined into a field loses that signal, so check the final URL, not the template.
Check 2: the response plays
- Feed a filled response through your actual player, not only through curl. Impression and quartile beacons fire from the player, and that is what reporting records.
- Test the no-fill path on purpose, for example with an off-hours request or a high
floor. The player should continue to content with no error shown to the viewer and no retry loop. - If you serve pods, check that slot 2 and later carry the same
pod, an incrementingslot, and a freshcb.
Check 3: end-to-end with Ezoic
Ezoic can see test traffic server-side and confirm what arrived. For launch verification, send your Ezoic representative:
- The approximate timestamp, with time zone, of your test session.
- Your bundle or domain.
- The device platform you tested from.
They can confirm that requests arrived with the expected signals and that auctions ran.
Common failure modes
| Symptom | Likely cause |
|---|---|
| Every request returns no fill | The placement is not yet active; or v=4 is sent to a player that parses only VAST 3, such as Roku; or the app is not registered or app-ads.txt is missing, so buyers will not bid. |
| Fill in development, none in production | Production sends a different identity, for example a hardcoded bundle that does not match the store, or a CMP that initializes only in development. |
| Fill on the web, none through a stitcher | The stitcher sends its own server IP and user agent, or its macros do not expand in its dialect. |
No fill with a very short tmax |
The auction budget is set below what the auction needs. Send your real budget. Keep it no lower than the stitcher's per-tag timeout allows. |
| Requests are up but reported impressions are lower | Impressions count when the ad renders and its beacons fire. Look for player errors on the creative, blocked beacons, or tracking events stripped during stitching. |
| Geo-targeted campaigns never serve | The viewer IP is not forwarded in a server-side call, or country disagrees with the IP. |
| Ads are always short | maxduration is set too low by a template. The default allows 120 seconds. |
Frequently asked questions
Is there a sandbox? No. Test against production with your real parameters. No-fill responses cost nothing, and Ezoic can verify test sessions server-side.
Do I need to handle HTTP errors?
Well-formed requests always return HTTP 200, after a possible 302. Your player needs two paths: VAST with an <Ad> plays, and an empty document continues to content. A 4xx, a 5xx or HTML means the URL itself is malformed.
The response has several <Ad> elements. Which one plays?
The first that renders. They are same-request fallbacks, not a pod. RAF and IMA handle this automatically.
Can I get several ads in one break?
Yes. Request each slot separately, with the same pod and an incrementing slot. One request returns one ad, and there is no VMAP.
Which parameters are required?
Mechanically, one identity lane: url or d for a site, or bundle with app_domain for an app. Commercially, send devicetype=3, ifa with ifa_type, the correct v, real w and h, and content metadata. Each one you omit shrinks the set of buyers who bid.
What are the default durations?
The tag accepts 5 to 120 second creatives by default. Send minduration or maxduration only if your breaks need something else.
Can I send skip?
Yes. Send skip=1 if your player renders a skip control. Buyers decide whether to send skippable creatives.
The viewer limited ad tracking. Should I drop ifa?
No. Send the zeroed ID the platform returns, with lmt=1. Dropping the identifier, or reporting limited tracking incorrectly, both look like errors to buyers.
My Roku sideloaded build sends bundle=dev. Is that a problem?
No. That is expected for development builds. Store installs report the real Channel ID.
My app is in beta. Can I monetize it? Not yet. Buyers cannot validate an unpublished app, because it has no public store listing. Test in beta, and monetization starts when the listing is public.
Where do I see revenue?
In your Ezoic dashboard, under the domain the integration is attached to: the url or d domain, or app_domain. CTV traffic is reported under the Connected TV device type.
Go-live checklist
- A real session request is captured, and every parameter is verified
- Fill and no-fill both handled cleanly in the player
- Ezoic has confirmed server-side receipt of a test session
- App registered under Mobile & CTV Apps, and app-ads.txt is live at least a few days before launch (app lane)
- Your Ezoic representative knows the launch date, so reporting is watched