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.

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>
VersionYearHeadline change
VAST 2.02009Introduced wrappers; established the basic InLine / Wrapper dichotomy
VAST 3.02012Ad pods, the Category element, richer error reporting via Error URLs
VAST 4.02016Split media files into the actual creative (MediaFile) and the interactive logic (InteractiveCreativeFile); introduced Universal Ad ID
VAST 4.12018Added the IAB Open Measurement (OMID) verification node, server-side ad insertion (SSAI) signaling
VAST 4.22019Added 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:

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:

  1. Parse the response.
  2. If the response contains <Wrapper> with a <VASTAdTagURI>, save any <Impression>, <Error>, <TrackingEvents>, and <ClickTracking> entries from this level to fire later.
  3. Make an HTTP GET to the VASTAdTagURI.
  4. Parse the response. If it is another wrapper, repeat.
  5. 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:

EventFires when
creativeViewThe creative is rendered (rarely used)
startPlayback begins, first frame painted
firstQuartile25% of duration played
midpoint50% of duration played
thirdQuartile75% of duration played
complete100% played (and not skipped)
mute / unmuteUser toggles sound
pause / resumeUser pauses or resumes
skipUser clicks the skip button
progressCustom 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:

CodeMeaningMost common cause
100XML parsing errorBad CDATA, unescaped ampersand, malformed XML
101Schema validation errorMissing required element (Impression, MediaFile)
102VAST version not supportedVAST 4.x served to a VAST 2-only player
300General wrapper errorWrapper response empty or unreadable
301Wrapper timeoutDownstream tag did not respond in time
302Wrapper limit reachedChain too deep
303No ads after one or more wrappersFinal response was an empty VAST
400General linear errorCatch-all when nothing more specific applies
402Media file URI not reachableCDN 404, broken signed URL, expired token
403Couldn't find supported MediaFilePlayer can't play any of the encodings offered
405Media file display errorCORS, mixed content, decryption failure

A practical debugging workflow

When a VAST tag is misbehaving, this is the order we go through:

  1. Fetch the tag URL with curl from 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.
  2. 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.
  3. Check the version attribute. Confirm it matches what the player supports (VAST.version).
  4. 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.
  5. Walk the wrapper chain. If the response is a wrapper, fetch its VASTAdTagURI and repeat. Note how deep you go. If you hit five levels and still don't have an InLine, that is your problem.
  6. Inspect the MediaFiles. Confirm there is at least one with type="video/mp4", and that the URL actually returns a video file (look at the Content-Type response header — it should be video/mp4, not text/html from an error page).
  7. 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).
  8. 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.


Related reading