View as Markdown

Roku (RAF)

This guide covers serving Ezoic video ads from a Roku channel with the Roku Advertising Framework (RAF). Client-side RAF is the recommended path. Stitched SSAI is also supported. See Server-side (SSAI) for that setup, and note that stitched pods are not eligible for Google demand.

The one rule that breaks everything if missed

Always send v=3. RAF parses VAST 2.0 and 3.0 only. The tag negotiates VAST 4.2 when v is omitted, and RAF fails to parse 4.x. Every request then looks like a no-fill.

Register the channel first

Register your channel in the Ezoic dashboard under Ezoic Ads → Mobile & CTV Apps → Add Mobile App, with platform Roku and your numeric Channel ID as the bundle. The store URL is optional, because it is derived from the Channel ID. Registration is needed for app-ads.txt approval. See App identity.

Tag template

https://g.ezoic.net/ezoic/videoad/vast?bundle=<CHANNEL_ID>&app_domain=<YOUR_EZOIC_DOMAIN>&app_name=<APP_NAME>&storeurl=https%3A%2F%2Fchannelstore.roku.com%2Fdetails%2F<CHANNEL_ID>&adunit=<YOUR_VIDEO_AD_UNIT>&make=Roku&os=Roku%20OS&ifa=ROKU_ADS_TRACKING_ID&ifa_type=rida&lmt=ROKU_ADS_LIMIT_TRACKING&devicetype=3&plcmt=1&v=3&w=1920&h=1080&startdelay=0&content_id=<ID>&content_title=<TITLE>&content_genre=<GENRE>&content_cat=<IAB_CAT>&content_len=<SECONDS>&livestream=0&us_privacy=<USP>&cb=<RANDOM>

ROKU_ADS_TRACKING_ID and ROKU_ADS_LIMIT_TRACKING are RAF macros. RAF substitutes the device's RIDA and limit-ad-tracking flag when the URL passes through setAdUrl(). Fill in everything else from your channel code. Use your normal video ad unit name for adunit. There is no separate CTV ad unit.

BrightScript integration

function PlayContentWithAds(video as object) as boolean
    adIface = Roku_Ads()
    adIface.setAdUrl(BuildEzoicTagUrl(video))
    adIface.enableAdMeasurements(true)
    adPods = adIface.getAds()

    if adPods <> invalid and adPods.Count() > 0
        watched = adIface.showAds(adPods, invalid, m.top.findNode("adContainer"))
        if not watched then return false
    end if
    ' An empty pod list is a normal no-fill. Play content.
    PlayContent(video)
    return true
end function

function BuildEzoicTagUrl(video as object) as string
    appInfo = CreateObject("roAppInfo")
    deviceInfo = CreateObject("roDeviceInfo")
    xfer = CreateObject("roUrlTransfer")

    url = "https://g.ezoic.net/ezoic/videoad/vast"
    url += "?bundle=" + appInfo.GetID()
    url += "&app_domain=" + xfer.Escape("<YOUR_EZOIC_DOMAIN>")
    url += "&app_name=" + xfer.Escape(appInfo.GetTitle())
    url += "&storeurl=" + xfer.Escape("https://channelstore.roku.com/details/" + appInfo.GetID())
    url += "&adunit=" + xfer.Escape("<YOUR_VIDEO_AD_UNIT>")
    url += "&ifa=ROKU_ADS_TRACKING_ID"
    url += "&ifa_type=rida"
    url += "&lmt=ROKU_ADS_LIMIT_TRACKING"
    url += "&devicetype=3&plcmt=1&v=3&w=1920&h=1080&startdelay=0"
    url += "&content_id=" + xfer.Escape(video.id)
    url += "&content_title=" + xfer.Escape(video.title)
    if video.genre <> invalid then url += "&content_genre=" + xfer.Escape(video.genre)
    if video.iabCategories <> invalid then url += "&content_cat=" + xfer.Escape(video.iabCategories)
    if video.length <> invalid then url += "&content_len=" + video.length.ToStr()
    url += "&livestream=0"
    url += "&cb=" + CreateObject("roDateTime").AsSeconds().ToStr()
    return url
end function

Key points:

  • Use appInfo.GetID() for the bundle. Store installs report the real Channel ID. Sideloaded builds report dev, which is expected in development. Do not hardcode the ID.
  • Pass the RAF macros literally. Put ROKU_ADS_TRACKING_ID and ROKU_ADS_LIMIT_TRACKING in the URL given to setAdUrl(). Do not fetch the RIDA yourself, and do not URL-encode the macro names.
  • RAF handles the response. VAST parsing, ad selection, beacon firing and rendering all happen inside showAds().

Behaviour your channel should expect

  • An empty pod list means no fill. Play content. Do not show an error to the viewer.
  • A response can contain several <Ad> elements. RAF plays the first that renders and falls back within the response when one fails.
  • One request is one ad break. For mid-rolls, build a fresh URL at each break with a new cb, the right startdelay, and pod and slot if you request several slots.

Roku-specific notes

  • No api parameter. Roku has no in-player measurement framework. The tag declares none for Roku-identified requests, so do not send api=7.
  • Privacy. Wire lmt through the RAF macro. Send us_privacy when you have a value. Send coppa=1 if the channel is child-directed. Roku certification also requires it for the Kids & Family category.
  • Ad IDs reset. Viewers can reset their RIDA. Treat the IFA as ephemeral and do not persist it.

Testing on hardware

  1. Sideload the build. The bundle reports dev. Fill can differ from production until the store-installed build runs.
  2. In the debug console, confirm the tag URL contains a real RIDA after RAF expands the macro, v=3, and your content metadata.
  3. getAds() returning a list, even an empty one, confirms the request reached the endpoint.
  4. After the channel is published, check a store-installed session. The bundle should report your real Channel ID.

Go-live checklist

  • Channel registered under Mobile & CTV Apps with platform Roku and the Channel ID
  • v=3 on every request
  • bundle read from roAppInfo.GetID(), not hardcoded
  • adunit set to your normal video ad unit name
  • RAF macros passed literally to setAdUrl()
  • devicetype=3, real w and h, startdelay sent
  • Content metadata populated from the feed
  • lmt wired through RAF; coppa=1 set if child-directed
  • A store-installed session verified with the real Channel ID