Understanding VAST tags: structure, wrapper resolution, and debugging
VAST is what makes a video ad play. It is also responsible for most of the avoidable problems in video monetisation. This is the field guide we wish we had when we first opened a broken tag at 2 a.m.
Contents
- What VAST is, in one paragraph
- VAST 2, 3 and 4: what actually changed
- Anatomy of a VAST document
- InLine ads — the leaf of the tree
- Wrapper ads and how players resolve them
- Tracking events and the verification panel
- VPAID, OMID, and the long goodbye
- The errors you will actually see
- A practical debugging workflow
What VAST is, in one paragraph
VAST stands for Video Ad Serving Template. It is an XML schema, published by the IAB Tech Lab, that describes a single video advertisement: where the video file is hosted, how long it should play, what the click-through URL is, what tracking pixels to fire at each event in the playback timeline, and what companion banners to show alongside it. A video player asks an ad server for an ad, receives a VAST XML document in response, parses it, and plays the ad according to the instructions inside.
That is the contract. The complexity comes from two facts: VAST has evolved through four major versions over fifteen years and the older versions still see live traffic, and a VAST response is often not a real ad but a wrapper pointing at another VAST URL, which in turn points at another wrapper, three or four levels deep.
VAST 2, 3 and 4: what actually changed
Knowing what version you are looking at is the first step in debugging anything. The root element of a VAST document always declares the version:
<VAST version="4.2">...</VAST>
| Version | Year | Headline change |
|---|---|---|
| VAST 2.0 | 2009 | Introduced wrappers; established the basic InLine / Wrapper dichotomy |
| VAST 3.0 | 2012 | Ad pods, the Category element, richer error reporting via Error URLs |
| VAST 4.0 | 2016 | Split media files into the actual creative (MediaFile) and the interactive logic (InteractiveCreativeFile); introduced Universal Ad ID |
| VAST 4.1 | 2018 | Added the IAB Open Measurement (OMID) verification node, server-side ad insertion (SSAI) signaling |
| VAST 4.2 | 2019 | Added macros for SSAI, more granular viewability event tracking |
In production today you will see all of these. Connected TV inventory leans towards VAST 4.x because OMID and SSAI matter there. Long-tail desktop video still serves a lot of VAST 2 and 3. A player that supports VAST 4 must, by spec, also support 2 and 3 — the schema is intentionally backwards compatible.
Anatomy of a VAST document
Every VAST document is structured the same way at the top level:
<VAST version="4.2">
<Ad id="acme-spring-15s" sequence="1">
<InLine>
<!-- ...the actual ad lives here... -->
</InLine>
</Ad>
</VAST>
Or, for a wrapper:
<VAST version="4.2">
<Ad id="ssp-passthrough-77821">
<Wrapper>
<VASTAdTagURI><![CDATA[https://dsp.example.com/vast?auc=abc]]></VASTAdTagURI>
<Impression><![CDATA[https://ssp.example.com/imp?auc=abc]]></Impression>
</Wrapper>
</Ad>
</VAST>
The <Ad> element contains exactly one of <InLine> or <Wrapper>. A VAST document can have multiple <Ad> elements; with sequence attributes set, that is an ad pod — a sequence of ads to be played back-to-back, the way three or four commercials run during a TV ad break.
InLine ads — the leaf of the tree
An <InLine> contains everything a player needs to actually show the ad. Stripped to its essentials:
<InLine>
<AdSystem>DSP Example v3</AdSystem>
<AdTitle>Acme Spring Sale 15s</AdTitle>
<Impression><![CDATA[https://dsp.example.com/imp?auc=abc]]></Impression>
<Error><![CDATA[https://dsp.example.com/err?code=[ERRORCODE]]]></Error>
<Creatives>
<Creative id="creative-44" adId="acme-spring-15s">
<Linear>
<Duration>00:00:15</Duration>
<TrackingEvents>
<Tracking event="start"><![CDATA[https://dsp.example.com/trk?e=start]]></Tracking>
<Tracking event="firstQuartile"><![CDATA[https://dsp.example.com/trk?e=q1]]></Tracking>
<Tracking event="midpoint"><![CDATA[https://dsp.example.com/trk?e=mid]]></Tracking>
<Tracking event="thirdQuartile"><![CDATA[https://dsp.example.com/trk?e=q3]]></Tracking>
<Tracking event="complete"><![CDATA[https://dsp.example.com/trk?e=done]]></Tracking>
</TrackingEvents>
<VideoClicks>
<ClickThrough><![CDATA[https://acme-advertiser.com/spring-sale]]></ClickThrough>
<ClickTracking><![CDATA[https://dsp.example.com/clk?auc=abc]]></ClickTracking>
</VideoClicks>
<MediaFiles>
<MediaFile delivery="progressive" type="video/mp4" width="1920" height="1080" bitrate="2500" scalable="true">
<![CDATA[https://cdn.example.com/spring-sale-15s-1080p.mp4]]>
</MediaFile>
<MediaFile delivery="progressive" type="video/mp4" width="1280" height="720" bitrate="1500">
<![CDATA[https://cdn.example.com/spring-sale-15s-720p.mp4]]>
</MediaFile>
</MediaFiles>
</Linear>
</Creative>
</Creatives>
</InLine>
The required elements inside an InLine are AdSystem, AdTitle, at least one Impression, and at least one Creative with a usable MediaFile. Everything else is optional, although a player that gets no tracking events will struggle to bill the impression — so in practice they are always present.
A few details that catch people out:
- Multiple
MediaFileentries let the player pick the best encoding for the device. The player will usually prefer the highest bitrate it can play; on a slow connection it may pick a lower-resolution variant. typematters.video/mp4is universally supported;video/webmandapplication/javascript(VPAID) are not, and putting only those in yourMediaFileslist will cause the ad to fail on many players.CDATAsections wrap every URL. Without them, an ampersand in a query string (which is everything, given the macros) breaks XML parsing.DurationisHH:MM:SSorHH:MM:SS.mmm. The format string00:15alone is invalid VAST and some strict players reject it.
Wrapper ads and how players resolve them
A Wrapper says "go fetch this other VAST URL." It exists because the real-time bidding chain is layered: the publisher's player calls the publisher's ad server, which calls an SSP, which holds an auction with several DSPs. Each step does not actually hold the video file — only the final DSP does. So each intermediate step responds with a wrapper pointing at the next step down.
Concretely, here is what a player does when it gets a wrapper:
- Parse the response.
- If the response contains
<Wrapper>with a<VASTAdTagURI>, save any<Impression>,<Error>,<TrackingEvents>, and<ClickTracking>entries from this level to fire later. - Make an HTTP GET to the
VASTAdTagURI. - Parse the response. If it is another wrapper, repeat.
- If it is an
<InLine>, fire the aggregated impression URLs (this level plus every wrapper above), play the media, and fire the tracking URLs in both the inline and every wrapper as the relevant events happen.
This is why a single video impression can fire fifteen or twenty pixels: each step in the chain has its own beacon, and they all want to know the ad played.
The wrapper limit problem. VAST 3 introduced the optional followAdditionalWrappers attribute and most player libraries also have an internal wrapper depth limit, typically 5 or 8. Long chains routinely hit it. When that happens you get a VAST error code 302 ("wrapper limit reached") and the impression silently dies. We have seen wrapper chains of 11 levels in the wild — usually three SSPs hop-rebidding into each other.
Tracking events and the verification panel
The list of tracking events a player will fire is fixed by the VAST spec. The most common ones:
| Event | Fires when |
|---|---|
creativeView | The creative is rendered (rarely used) |
start | Playback begins, first frame painted |
firstQuartile | 25% of duration played |
midpoint | 50% of duration played |
thirdQuartile | 75% of duration played |
complete | 100% played (and not skipped) |
mute / unmute | User toggles sound |
pause / resume | User pauses or resumes |
skip | User clicks the skip button |
progress | Custom time offset reached (with offset attribute) |
Each <Tracking> element is a single URL. The player fires it with an HTTP GET (some implementations use a hidden img tag, others use fetch() in keepalive mode — neither expects a meaningful response).
VAST 4 added an <AdVerifications> node — a list of third-party JavaScript files (typically from measurement vendors like IAS, DV, or Moat) that the player is asked to load alongside the ad. These run inside an OMID-compliant container and produce viewability and brand-safety measurements. The presence of AdVerifications is one quick signal that you are looking at a modern VAST 4 ad.
VPAID, OMID, and the long goodbye
VPAID — Video Player-Ad Interface Definition — is a JavaScript interface that lets an ad take over the video player to inject interactivity, run custom measurement, or do server-side decisioning during playback. In VAST it shows up as a MediaFile with type="application/javascript" and an apiFramework="VPAID" attribute.
<MediaFile delivery="progressive" type="application/javascript"
apiFramework="VPAID" width="640" height="360">
<![CDATA[https://cdn.example.com/vpaid-wrapper.js]]>
</MediaFile>
VPAID solved real problems — pre-OMID, it was the only way to do real viewability measurement on video. But it also created huge problems: a malicious or buggy VPAID could exfiltrate page data, run a crypto-miner, or simply break the player. Major platforms — CTV first, then YouTube — deprecated VPAID through the late 2010s. The IAB's replacement is OMID (Open Measurement Interface Definition), which exposes only the data the player chooses to share, in a structured way, instead of giving JavaScript full reign.
By 2026, VPAID is almost entirely absent from CTV and mobile-app video, and increasingly rare even on desktop. If you are designing new video systems, do not add VPAID support; do support OMID via the <AdVerifications> node in VAST 4.
The errors you will actually see
The VAST spec defines a set of standardised error codes. When a player can't play an ad it substitutes the code into the [ERRORCODE] macro of any <Error> URL and fires it. The codes most likely to land in your logs:
| Code | Meaning | Most common cause |
|---|---|---|
100 | XML parsing error | Bad CDATA, unescaped ampersand, malformed XML |
101 | Schema validation error | Missing required element (Impression, MediaFile) |
102 | VAST version not supported | VAST 4.x served to a VAST 2-only player |
300 | General wrapper error | Wrapper response empty or unreadable |
301 | Wrapper timeout | Downstream tag did not respond in time |
302 | Wrapper limit reached | Chain too deep |
303 | No ads after one or more wrappers | Final response was an empty VAST |
400 | General linear error | Catch-all when nothing more specific applies |
402 | Media file URI not reachable | CDN 404, broken signed URL, expired token |
403 | Couldn't find supported MediaFile | Player can't play any of the encodings offered |
405 | Media file display error | CORS, mixed content, decryption failure |
A practical debugging workflow
When a VAST tag is misbehaving, this is the order we go through:
- Fetch the tag URL with
curlfrom the same network you expect the player to use. A surprising number of "broken tag" tickets turn out to be the ad server returning a 500, a 302 redirect loop, or a CORS-rejected response. - Pretty-print the response. Pipe it through an XML formatter, or paste it into a structured viewer. Single-line VAST is unreadable. If the response is JSON-wrapped (some bidders do this), peel the JSON off first.
- Check the version attribute. Confirm it matches what the player supports (
VAST.version). - Validate against the schema. The IAB publishes XSDs for each VAST version. A schema check catches missing elements, wrongly nested children, and invalid attribute values.
- Walk the wrapper chain. If the response is a wrapper, fetch its
VASTAdTagURIand repeat. Note how deep you go. If you hit five levels and still don't have anInLine, that is your problem. - Inspect the
MediaFiles. Confirm there is at least one withtype="video/mp4", and that the URL actually returns a video file (look at the Content-Type response header — it should bevideo/mp4, nottext/htmlfrom an error page). - Hit one of the tracking URLs. If it returns 200 with no body, you are fine. If it returns 401, the macros may not be substituting (some tracker domains require a signed parameter).
- Diff against a known-good tag. Take a VAST response from the same SSP that did play, and diff it against the broken one. Often the difference is one missing attribute.
Steps 2 and 8 are exactly what we built the JSON / text diff tool for. The URL decoder is useful when wrapper URLs come back triple-encoded. And the in-browser OpenRTB creative renderer happily takes a VAST string in the adm field and plays it inside an isolated iframe — much faster than spinning up a player in a test page.
Tip: when fetching wrapper URLs by hand, add a realistic User-Agent header and a referer that matches the publisher you are debugging on. Many DSPs gate their VAST endpoints by these and will return an empty response to a bare curl.
Closing thought
VAST is a less elegant protocol than OpenRTB. It accumulated history; it is XML in a world that has mostly settled on JSON; the wrapper model produces failure modes that exist nowhere else in advertising. But once you can walk a wrapper chain by hand and predict which errors will come from where, video debugging stops feeling like archeology and starts feeling like ordinary engineering.