VAST Tag Integration: CTV Platform Setup

Request LtvAdx VAST 4.2 ads from your CTV app or FAST channel player. The ad is decided when the request arrives, and the player reports playback through the tracking URLs in the response.

How it works

Your player calls the VAST endpoint at each ad break. LtvAdx checks the channel, runs the line-item filters, and returns VAST 4.2 XML with the media file, signed tracking URLs and a click-through. The player fires the impression and quartile URLs from the device.

Step 1 — Create a channel

  1. Sign in to the dashboard and open Publisher Portal → Channels.
  2. Create a channel: platform, content model (AVOD, FAST, BVOD or LINEAR_TV), category and floor CPM.
  3. Wait for admin approval — an unapproved or inactive channel always gets an empty VAST.
  4. Copy the Channel ID.

Step 2 — Build the VAST URL

These are the parameters /vast reads. Anything else on the URL is ignored.

ParameterTypeDescription
channelId *stringYour channel ID. Without it the response is an empty VAST.
householdIdstringA stable household id from your player or backend. Enables frequency caps and audience targeting; without it the request is anonymous.
platformenumROKU | FIRE_TV | APPLE_TV | SAMSUNG_TIZEN | LG_WEBOS | ANDROID_TV | LINEAR | WEB. A request without it matches any platform targeting.
breakTypeenumPRE_ROLL (default) | MID_ROLL | POST_ROLL | BUMPER
durationintegerSeconds available in the break; line items longer than this are skipped.
podDuration / pmxdseconds / msAd pod: one request fills a whole avail of this length with sequenced ads (see Ad pods below).
maxAds / pmadintegerAd pod: at most this many ads (default 4, never more than 10).
ip, uastringThe viewer's IP and user agent — used only when the request comes from an SSAI stitcher LtvAdx has vetted for your account; ignored from anyone else.

adBreakId and dnt are accepted but not used in the decision today. Device advertising IDs, consent strings and content genre are not read by /vast.

GET https://ads.ltvadx.com/vast
    ?channelId=YOUR_CHANNEL_ID
    &householdId=YOUR_HOUSEHOLD_ID
    &platform=ROKU
    &breakType=MID_ROLL
    &duration=30

Step 3 — Handle the response

The status is always 200. A filled response contains an <Ad>; a no-ad response is an empty <VAST version="4.2"/>. Both carry an X-Request-Id header (and filled ones an X-Ad-Decision header with the winning deal type). Abridged filled response:

<VAST version="4.2" xmlns="http://www.iab.com/VAST">
  <Ad id="ad-REQUEST_ID" sequence="1">
    <InLine>
      <AdSystem version="2.0">LtvAdx</AdSystem>
      <Error><![CDATA[https://ads.ltvadx.com/track/REQUEST_ID?hh=…&crid=…&liid=…&chid=…&sig=…&event=error&code=[ERRORCODE]]]></Error>
      <Impression id="imp-REQUEST_ID"><![CDATA[https://ads.ltvadx.com/track/REQUEST_ID?hh=…&crid=…&liid=…&chid=…&sig=…&event=impression]]></Impression>
      <AdServingId>REQUEST_ID</AdServingId>
      <AdTitle><![CDATA[Spot name]]></AdTitle>
      <Creatives>
        <Creative id="cre-CREATIVE_ID" sequence="1">
          <Linear>
            <TrackingEvents>
              <Tracking event="start"><![CDATA[…&event=start]]></Tracking>
              <Tracking event="firstQuartile"><![CDATA[…&event=firstQuartile]]></Tracking>
              <Tracking event="midpoint"><![CDATA[…&event=midpoint]]></Tracking>
              <Tracking event="thirdQuartile"><![CDATA[…&event=thirdQuartile]]></Tracking>
              <Tracking event="complete"><![CDATA[…&event=complete]]></Tracking>
            </TrackingEvents>
            <Duration>00:00:30</Duration>
            <MediaFiles>…</MediaFiles>
            <VideoClicks>
              <ClickTracking><![CDATA[…&event=click]]></ClickTracking>
              <ClickThrough><![CDATA[advertiser landing page]]></ClickThrough>
            </VideoClicks>
          </Linear>
          <UniversalAdId idRegistry="LtvAdx">CREATIVE_ID</UniversalAdId>
        </Creative>
      </Creatives>
    </InLine>
  </Ad>
</VAST>

Every response validates against the IAB VAST 4.2 schema. Web players on your own site (Google IMA, video.js, HTML5 TV apps) can read it: /vast and /vmap answer CORS for any origin, with credentials.

Fire the URLs exactly as given

Tracking URLs are signed. A modified URL is still recorded but never billed, so the advertiser is not charged and you are not paid for that impression.

Ad pods for your SSAI stitcher

If you already run a stitcher — AWS MediaTailor, Google DAI with a third-party ad server, Broadpeak, Harmonic — point its ad decision server at /vast with podDuration and maxAds. One request returns a VAST 4.2 ad pod: up to maxAds ads in sequence whose durations fit the avail, never the same campaign or advertiser twice, each with its own signed tracking. A pod can be shorter than the avail when demand runs out; your stitcher fills the rest. Your stitcher transcodes the MediaFiles itself.

# AWS MediaTailor — ad decision server URL, with its session variables
https://ads.ltvadx.com/vast?channelId=YOUR_CHANNEL_ID&breakType=MID_ROLL
  &podDuration=[session.avail_duration_secs]&maxAds=4
  &householdId=[player_params.hh]&platform=[player_params.platform]
  &ip=[session.client_ip]&ua=[session.user_agent]

The viewer's address

A stitcher calls from its own servers. Tell us its address ranges and we will use the viewer IP and user agent it passes (ip / ua, or the X-Device-IP / X-Device-User-Agent headers of the IAB SSAI guidance) for country targeting and invalid-traffic checks — on ad requests and on the beacons it fires. From any other address they are ignored.

Step 4 — Platform notes

Roku (BrightScript / SceneGraph)

' BrightScript — build the VAST URL
Function buildVastUrl(channelId As String, hhId As String) As String
    url = "https://ads.ltvadx.com/vast"
    url += "?channelId=" + channelId
    url += "&householdId=" + hhId
    url += "&platform=ROKU"
    url += "&breakType=MID_ROLL"
    url += "&duration=30"
    Return url
End Function

Android TV / Fire TV (IMA SDK)

// Kotlin
val vastUrl = "https://ads.ltvadx.com/vast" +
    "?channelId=$channelId" +
    "&householdId=$householdId" +
    "&platform=ANDROID_TV" +   // or FIRE_TV
    "&breakType=PRE_ROLL" +
    "&duration=30"

val adsRequest = AdsRequest().apply { adTagUrl = vastUrl }
adsLoader.requestAds(adsRequest)

Samsung Tizen / LG webOS

Use your player's VAST support (for example Shaka or a Video.js IMA plugin) with platform=SAMSUNG_TIZEN or platform=LG_WEBOS.

Step 5 — VMAP for long-form content

/vmap returns a VMAP 1.0.1 playlist with three breaks — pre-roll, one mid-roll at half the content duration, and post-roll. Each break is a /vast tag, decided when the player calls it, with one ad.

GET https://ads.ltvadx.com/vmap
    ?channelId=YOUR_CHANNEL_ID
    &householdId=YOUR_HOUSEHOLD_ID
    &contentDurationSeconds=3600     # default 3600; the mid-roll goes at 00:30:00

<vmap:VMAP version="1.0.1" xmlns:vmap="http://www.iab.net/videosuite/vmap">
  <vmap:AdBreak timeOffset="start" breakType="linear" breakId="preroll">
    … <VASTAdTagURI>https://ads.ltvadx.com/vast?channelId=…&householdId=…&breakType=PRE_ROLL</VASTAdTagURI> …
  </vmap:AdBreak>
  <vmap:AdBreak timeOffset="00:30:00" breakType="linear" breakId="midroll-1"> … MID_ROLL … </vmap:AdBreak>
  <vmap:AdBreak timeOffset="end" breakType="linear" breakId="postroll"> … POST_ROLL … </vmap:AdBreak>
</vmap:VMAP>

Testing your integration

  1. Copy a pre-filled URL from Publisher Portal → VAST Tag Generator.
  2. Request it: curl -sD - "URL". An empty <VAST> means no line item is eligible for this channel yet — note the X-Request-Id.
  3. Ask your account contact for a test campaign targeting your channel, then retest.
  4. Play the ad on a device and confirm impressions appear in your publisher reports within a few minutes.

Why a request got no ad

Common causes: the channel is not approved, no active line item targets this channel, platform or break type, all bids are below the floor CPM, or the household reached its frequency cap. Send us the X-Request-Id and we can show exactly which check stopped each line item.