What this page covers
- VAST 4.1 InLine structure with every required and key optional element.
- Tracking event reference — which events must fire and when.
- Wrapper chains: how they work, how many hops are too many, and how they fail.
Learning Track
Read, validate, and debug VAST 4.1 tags — tracking events, MediaFile requirements, wrapper chains, and CTV delivery failures.
Intermediate track
This page starts at an intermediate level. You already know that VAST serves video ads. The goal here is to help you read VAST tags line by line, validate event coverage, understand wrapper chains, and connect CTV failures back to specific XML problems.
VAST (Video Ad Serving Template) is the XML standard that tells a video player how to play an ad: what video file to load, how long it runs, which URLs to ping at each quartile, and where to send the user when they click. If any element is wrong — a missing Impression URL, a tracking event with a malformed URL, a MediaFile with an unsupported codec for CTV — the ad either doesn’t play or plays without reporting delivery back to the DSP. TAMs diagnose these issues daily when advertisers report discrepancies between their ad server numbers and the publisher’s reporting.
Ad elements.InLine (the actual creative) or Wrapper (a redirect to another VAST).event attribute and a URL. The player fires each URL at the corresponding moment during playback.A complete, production-valid VAST 4.1 tag for a 30-second skippable linear video ad. Every element is explained inline.
<?xml version="1.0" encoding="UTF-8"?>
<VAST version="4.1">
<Ad id ="ad-001" sequence ="1" > <!-- sequence: pod position (1 = first ad) -->
<InLine>
<AdSystem version ="4.1" > My Ad Server</AdSystem>
<AdTitle> Samsung TV - 30s Spot</AdTitle>
<!-- Impression: fires when the player confirms it has loaded the VAST and is ready to play -->
<Impression id ="imp-001" >
<![CDATA[https://track.example.com/imp?aid=001&cb=__random__]]>
</Impression>
<!-- Second impression URL — both fire on the same event (common for dual-pixel attribution) -->
<Impression id ="imp-002" >
<![CDATA[https://dsp.example.com/imp?crid=55&type=vast]]>
</Impression>
<AdVerifications> <!-- OMID viewability — required for IAS, DoubleVerify integration -->
<Verification vendor ="doubleverify.com-omid" >
<JavaScriptResource apiFramework ="omid" browserOptional ="true" >
<![CDATA[https://cdn.doubleverify.com/dvtp_src.js]]>
</JavaScriptResource>
</Verification>
</AdVerifications>
<Creatives>
<Creative id ="cr-001" sequence ="1" adId ="ad-001" >
<Linear skipoffset ="00:00:05" > <!-- Skip button appears after 5 seconds -->
<Duration> 00:00:30</Duration> <!-- HH:MM:SS — must match actual video duration -->
<TrackingEvents>
<!-- Required IAB events: muted, unmuted, pause, resume, rewind, skip, playerExpand, playerCollapse -->
<!-- Quartile events below are the most tracked by DSPs -->
<Tracking event="start"> <![CDATA[https://track.example.com/start?cb=__r__]]> </Tracking>
<Tracking event="firstQuartile"> <![CDATA[https://track.example.com/q1?cb=__r__]]> </Tracking>
<Tracking event="midpoint"> <![CDATA[https://track.example.com/mid?cb=__r__]]> </Tracking>
<Tracking event="thirdQuartile"> <![CDATA[https://track.example.com/q3?cb=__r__]]> </Tracking>
<Tracking event="complete"> <![CDATA[https://track.example.com/complete?cb=__r__]]> </Tracking>
<Tracking event="mute"> <![CDATA[https://track.example.com/mute?cb=__r__]]> </Tracking>
<Tracking event="unmute"> <![CDATA[https://track.example.com/unmute?cb=__r__]]> </Tracking>
<Tracking event="pause"> <![CDATA[https://track.example.com/pause?cb=__r__]]> </Tracking>
<Tracking event="resume"> <![CDATA[https://track.example.com/resume?cb=__r__]]> </Tracking>
<Tracking event="skip"> <![CDATA[https://track.example.com/skip?cb=__r__]]> </Tracking>
<Tracking event="progress" offset="00:00:10"> <!-- Custom progress at 10s -->
<![CDATA[https://track.example.com/10s?cb=__r__]]>
</Tracking>
</TrackingEvents>
<MediaFiles>
<!-- Primary: HD MP4 for web and CTV -->
<MediaFile
delivery ="progressive" <!-- progressive = download; streaming = HLS/DASH -->
type ="video/mp4"
width ="1920" height ="1080"
bitrate ="2500" <!-- Kbps — player picks closest match to connection speed -->
codec ="H.264" >
<![CDATA[https://cdn.example.com/ads/spot-1080p.mp4]]>
</MediaFile>
<!-- Fallback: lower bitrate for slower connections -->
<MediaFile
delivery ="progressive"
type ="video/mp4"
width ="1280" height ="720"
bitrate ="1200"
codec ="H.264" >
<![CDATA[https://cdn.example.com/ads/spot-720p.mp4]]>
</MediaFile>
<!-- VPAID/SIMID interactive: for desktop only — avoid on CTV -->
<InteractiveCreativeFile
type ="text/javascript"
apiFramework ="simid"
variableDuration ="false" >
<![CDATA[https://cdn.example.com/ads/simid.js]]>
</InteractiveCreativeFile>
</MediaFiles>
<VideoClicks>
<ClickThrough id ="ct-001" >
<![CDATA[https://advertiser.example.com/landing?src=ctv]]>
</ClickThrough>
<ClickTracking id ="ctr-001" > <!-- Fires on click alongside ClickThrough -->
<![CDATA[https://track.example.com/click?aid=001]]>
</ClickTracking>
</VideoClicks>
</Linear>
</Creative>
</Creatives>
<Extensions>
<Extension type ="iab-Count" >
<Count type ="impression" > 1</Count>
</Extension>
</Extensions>
</InLine>
</Ad>
</VAST>
<![CDATA[url]]>. Without CDATA, URL characters like &, <, > break XML parsing and cause a VAST 400 error. This is one of the most common VAST bugs.video/mp4, video/webm. Incorrect MIME type causes the player to reject the MediaFile even if the actual video would play fine.start tracking event fires when the first frame plays. These are different events — a discrepancy between impression count and start count indicates ads loading but not playing.A Wrapper is a VAST response that redirects to another VAST URL instead of containing an InLine creative. Wrappers are used by ad networks to pass through demand to underlying DSPs. They add latency with each hop.
<?xml version="1.0" encoding="UTF-8"?>
<VAST version="4.1">
<Ad id ="wrapper-001" >
<Wrapper followAdditionalWrappers ="true" >
<!-- followAdditionalWrappers: true = keep following Wrapper chains; false = stop -->
<AdSystem> SSP Reseller Layer</AdSystem>
<VASTAdTagURI>
<!-- URL the player/SSAI fetches next — this is the "next hop" in the chain -->
<![CDATA[https://dsp.example.com/vast?bid=xyz&cb=__random__]]>
</VASTAdTagURI>
<!-- Impression from the wrapper layer — fires in addition to InLine impressions -->
<Impression> <![CDATA[https://ssp.example.com/imp?w=1]]> </Impression>
<Creatives>
<Creative><Linear>
<TrackingEvents>
<!-- Wrapper-level tracking fires alongside InLine tracking -->
<Tracking event="complete"> <![CDATA[https://ssp.example.com/complete?w=1]]> </Tracking>
</TrackingEvents>
</Linear></Creative>
</Creatives>
</Wrapper>
</Ad>
</VAST>
<!-- ===== Chain resolves to InLine after 2 more Wrapper hops: =====
Hop 1: Publisher ad server → SSP Wrapper (this file)
Hop 2: SSP → DSP Wrapper
Hop 3: DSP → InLine (final creative, media files, all tracking URLs)
Impressions that fire:
- publisher-ad-server impression
- ssp.example.com/imp?w=1
- dsp.example.com/imp (from hop 2 wrapper)
- dsp.example.com/imp (from InLine)
Total: 4 impression pixels for one ad play.
===== -->
VAST 4.1 defines these player-fired events. All URLs must be inside CDATA. Quartile events are the most commonly audited — missing any of them causes DSP reporting discrepancies.
| Event name | When it fires | Required? | Common issue |
|---|---|---|---|
| impression | VAST loaded by player (before playback) | Yes | Fires even if ad never plays — not a playback confirmation |
| start | First frame of video renders | Yes | Gap between impression and start count = ads loaded but not playing |
| firstQuartile | 25% of duration played | Yes | Missing = DSP cannot calculate view-through attribution at 25% |
| midpoint | 50% of duration played | Yes | Most DSPs bill on midpoint — missing means no billing signal |
| thirdQuartile | 75% of duration played | Yes | Used for viewability measurement |
| complete | 100% of ad played | Yes | Brands report completion rate — missing breaks campaign reporting |
| mute / unmute | User clicks mute/unmute button | Recommended | Missing = brand engagement data incomplete |
| pause / resume | User pauses/resumes playback | Recommended | Pause during ad does not reset quartile tracking — player must resume from where it paused |
| skip | User clicks skip button | If skipoffset set | Must be present if ad is skippable. Missing = no skip reporting to DSP |
| progress | Custom offset (e.g. 10s mark) | Optional | Use for custom measurement points (e.g. “watched 10s of a 30s ad”) |
| clickThrough | User clicks the ad creative | Optional (but tracked) | CTV remote clicks do not trigger clickThrough — CTV campaigns often show 0 clicks |
The <Error> element in a VAST tag defines a URL that fires when the player encounters an error. The [ERRORCODE] macro is replaced by the player with the numeric error code.
<!-- Add inside <InLine> or <Wrapper> to receive error notifications -->
<Error>
<![CDATA[https://track.example.com/vast-error?code=[ERRORCODE]&aid=001]]>
</Error>
<!-- The player replaces [ERRORCODE] with the numeric error at runtime -->
<!-- Example fired URL: https://track.example.com/vast-error?code=403&aid=001 -->
| Code | Meaning | Common cause | Fix |
|---|---|---|---|
| 100 | XML parse error | Malformed XML, unescaped & in URL | Validate with IAB VAST Inspector. Ensure all URLs are CDATA-wrapped. |
| 101 | VAST schema validation error | Required element missing (e.g. <Duration>) | Run schema validation. Confirm InLine has AdSystem, Impression, Creatives, Duration, MediaFile. |
| 102 | VAST version not supported | Player supports VAST 2.0 only; tag uses VAST 4.1 elements | Provide VAST 2.0 fallback for older players. CTV players often need 4.0+ for proper tracking. |
| 200 | Trafficking error | Wrong ad unit trafficked to wrong player | Verify the ad unit type matches player type (linear video for in-stream player). |
| 300 | Wrapper timeout | Wrapper URL did not respond in time | Reduce wrapper chain depth. Increase player VAST timeout if configurable. Check DSP latency. |
| 301 | Wrapper no ads | Wrapped VAST returned empty response | Check if the DSP has fill for the request parameters. Add a fallback VAST URL on the SSP side. |
| 303 | No VAST after max wrappers | Chain exceeded player’s max hop limit (typically 5) | Flatten the chain. Ask the DSP to return InLine directly or limit to 2 wrapper hops. |
| 400 | General MediaFile error | File 404, wrong MIME type, or unsupported codec | Confirm the MediaFile URL resolves. Verify MIME type is video/mp4. For CTV: confirm H.264. |
| 401 | MediaFile timeout | Video file loaded too slowly for the player buffer | Move creative to a CDN closer to viewer geography. Check bitrate — 2500 Kbps may be too high for some connections. |
| 403 | MediaFile unsupported | Player cannot play any of the provided MediaFiles | Add multiple MediaFile entries covering different codecs and bitrates. CTV always needs H.264 MP4. |
| 900 | Undefined error | Player-specific issue not covered by IAB codes | Check player SDK release notes. Capture network HAR and player console log for the session. |
VAST was loaded by the player (impression pixel hit) but playback never started. On CTV: the MediaFile codec or format was unsupported. Error code 403 in the error pixel. On web: VPAID/SIMID JS failed to load.
▸ Check MediaFile elements for H.264 MP4. Confirm delivery=“progressive” for CTV (not streaming). Validate that all MediaFile URLs are accessible from the player’s network — geo blocks or signed URL expiry are common culprits.
DSP reports completion rate is 0% but the ad is playing fully. The complete tracking URL is malformed (unescaped characters) or the tracking domain is blocked by the CTV device’s network.
▸ Validate the complete tracking URL by pasting it directly into a browser. Check for & characters that should be & in XML. Test from a CTV device’s network IP — some enterprise networks block third-party tracking domains.
The player followed the wrapper chain but hit the hop limit (typically 5) before reaching an InLine. The wrapper chain is too deep — common when multiple layers of resellers are in the supply path.
▸ Use a VAST inspector tool to resolve the full wrapper chain manually. Count the hops. Ask the DSP to reduce to 2 hops maximum. If SSAI, confirm it is configured to follow at least 5 redirects.
When validating a VAST tag, some quartile events are missing from the XML entirely. The DSP will show 0% completion rate or 0% quartile measurement even if the ad plays correctly.
▸ Run the tag through the IAB VAST Inspector — it reports missing required events. For each missing event, add a Tracking element with the correct event attribute and a valid CDATA-wrapped URL. All 4 quartile events + complete are minimum required.
VAST Duration says 00:00:30 but the video file is 29.97s or 31.5s. Some players cut the ad short at the declared duration; others wait for the full file. Creates inconsistent reporting and viewing experience.
▸ Inspect the actual MediaFile duration with ffprobe or a media info tool. Update VAST Duration to match. Always encode ads at exact target durations (30.000s, not 29.97s) before trafficking.
CTV remote controls register clicks differently than mouse clicks. The VideoClicks/ClickThrough event may never fire, or fires but is not tracked. Many CTV platforms handle click-to-website differently (QR code, second screen).
▸ This is expected behavior on most CTV devices — do not report it as a bug. Instead, confirm whether the CTV device supports “click-to-QR” or “send to phone” features, and set up tracking for those interactions separately in the creative companion.
vast-template.xml — reusable template for future QA.vast-audit.csv — tag, events present, hops, CTV compatible, issues.vast-error-cheat-sheet.md — code, cause, fix, test method.Enter any two values
to calculate the third
More tools coming soon