QR codes and shoppable CTV in VAST
A television has no cursor, no keyboard worth using, and a remote control with a directional pad. The QR code exists because the viewer already holds a better input device. Point the phone at the screen and the interaction moves to a surface designed for it.
Until the CTV Ad Portfolio work, QR codes in CTV creative were burned into the video or hacked into a vendor extension, which meant nobody downstream could read the destination, position or size. The draft schema adds four typed elements in a <CreativeExtension>, so the QR becomes structured data rather than pixels.
Quick reference
| Container | <CreativeExtension type="tl_qrcode"> |
| Required in practice | Scan URL and image URL |
| Position units | Percentages only. Pixels are invalid |
| Size units | Percentage |
| Best placements | Pause, screensaver, and other idle-state formats |
The markup
<CreativeExtension type="tl_qrcode">
<QrCodeScanUrl><![CDATA[https://brand.example.com/qr]]></QrCodeScanUrl>
<QrCodeImageUrl><![CDATA[https://cdn.example.com/qr.png]]></QrCodeImageUrl>
<QrCodePosition xPosition="10%" yPosition="70%"/>
<QrCodeSize size="15%"/>
</CreativeExtension>QrCodeScanUrlis where the scan lands. This is the destination a phone opens, and it is the field that should carry your tracking parameters.QrCodeImageUrlis the rendered code image the player draws. Supplying it rather than asking the player to generate one keeps the visual identical across devices.QrCodePositionplaces the code as a percentage of the screen.QrCodeSizesizes it as a percentage.
The trap: pixels are valid on icons and invalid here
<Icon> accepts an integer or a percentage for xPosition and yPosition, so xPosition="120" is valid there. <QrCodePosition> accepts percentages only, and a team that starts from working icon markup carries the pixel habit across. A code 120 pixels from the left sits somewhere different on a 1080p panel than on a 4K one, which is presumably why the schema closed the door.
VASTlint reports it as VAST-4.4-qrcode-position-percent at error severity, since the markup is malformed rather than merely unwise. The schema-level detail is in the 4.4 reference.
The whole creative, end to end
The QR block does not stand alone. It sits inside <CreativeExtensions> on the creative that carries the ad, which for shoppable CTV is almost always a pause unit. Element order matters: <UniversalAdId>, then <CreativeExtensions>, then <NonLinearAds>.
<VAST version="4.2" xmlns="http://www.iab.com/VAST">
<Ad id="shoppable-pause-1">
<InLine>
<AdSystem version="1.0">My Ad Server</AdSystem>
<AdTitle>Shoppable pause</AdTitle>
<AdServingId>f4a1-shop-0012</AdServingId>
<Impression><![CDATA[https://track.example.com/imp]]></Impression>
<Creatives>
<Creative id="c1">
<UniversalAdId idRegistry="Ad-ID">SHOP4455</UniversalAdId>
<CreativeExtensions>
<CreativeExtension type="tl_qrcode">
<QrCodeScanUrl><![CDATA[https://brand.example.com/qr?cid=SHOP4455&src=ctv]]></QrCodeScanUrl>
<QrCodeImageUrl><![CDATA[https://cdn.example.com/qr-shop4455.png]]></QrCodeImageUrl>
<QrCodePosition xPosition="72%" yPosition="18%"/>
<QrCodeSize size="18%"/>
</CreativeExtension>
</CreativeExtensions>
<NonLinearAds>
<NonLinear width="1920" height="1080">
<Duration>00:00:15</Duration>
<MediaFiles>
<MediaFile delivery="progressive" type="video/mp4" width="1920" height="1080">
<![CDATA[https://cdn.example.com/pause-shop.mp4]]>
</MediaFile>
</MediaFiles>
<NonLinearClickThrough><![CDATA[https://brand.example.com/offer]]></NonLinearClickThrough>
</NonLinear>
<TrackingEvents>
<Tracking event="creativeView"><![CDATA[https://track.example.com/view]]></Tracking>
<Tracking event="overlayViewDuration"><![CDATA[https://track.example.com/duration]]></Tracking>
</TrackingEvents>
</NonLinearAds>
</Creative>
</Creatives>
<Extensions>
<Extension type="plcmt" ext="adcom"><plcmt>5</plcmt></Extension>
<Extension type="pos" ext="adcom"><pos>7</pos></Extension>
<Extension type="playbackmethod" ext="adcom"><playbackmethod>9</playbackmethod></Extension>
<Extension type="attr" ext="adcom"><attr>21</attr></Extension>
</Extensions>
</InLine>
</Ad>
</VAST>Two details in there are easy to miss. The scan URL carries campaign parameters, because the scan is the only conversion event this format produces and an unparameterised URL loses the attribution. And the QR sits at 72 percent across and 18 percent down, top right, away from the lower third where most publishers draw their own chrome.
The other four checks
| Rule | What it catches |
|---|---|
| VAST-4.4-qrcode-missing-scan-url | A QR block with an image and no destination. The code renders, the scan goes nowhere. |
| VAST-4.4-qrcode-missing-image-url | A destination with no image. Whether anything draws depends entirely on the player. |
| VAST-4.4-qrcode-position-attrs | A position element missing one of its two coordinates. |
| VAST-4.4-qrcode-size-percent | A size expressed in something other than a percentage. |
Where a QR belongs
Placement is a creative decision with a hard constraint behind it: the viewer needs time to get their phone out. A code on screen for three seconds during a mid-roll is decoration. A code on a pause ad or a screensaver is usable, because the viewer has stopped and the ad persists.
The guidance names idle-state placements as the natural home for the same reason, and it explains why most of the shoppable CTV work across streamers has concentrated on pause.
What the tag proves about a shoppable ad
A check on the document confirms that the QR block is well formed, that a destination exists, that the image URL is present and served over HTTPS, that the geometry uses units the schema accepts, and that the NonLinear the code sits on has a renderable asset.
None of that tells you the code scans. Contrast, size on a specific panel, viewing distance and whether the destination page loads on a phone all have to be tested on a device. The document-level checks exist so you do not spend that device time on a creative that was malformed before it left the ad server.
Common mistakes checklist
- Pixel positions copied from icon markup. The most common failure, and an error rather than a warning.
- A scan URL with no tracking parameters. The scan is the conversion event. Instrument it.
- QR on a short mid-roll. Nobody has time to scan it.
- Relying on the player to generate the code image. Supply the image and the creative looks the same everywhere.
- An HTTP image or destination URL. Same mixed content rules as every other asset in the tag.