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 reportdev, which is expected in development. Do not hardcode the ID. - Pass the RAF macros literally. Put
ROKU_ADS_TRACKING_IDandROKU_ADS_LIMIT_TRACKINGin the URL given tosetAdUrl(). 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 rightstartdelay, andpodandslotif you request several slots.
Roku-specific notes
- No
apiparameter. Roku has no in-player measurement framework. The tag declares none for Roku-identified requests, so do not sendapi=7. - Privacy. Wire
lmtthrough the RAF macro. Sendus_privacywhen you have a value. Sendcoppa=1if 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
- Sideload the build. The bundle reports
dev. Fill can differ from production until the store-installed build runs. - In the debug console, confirm the tag URL contains a real RIDA after RAF expands the macro,
v=3, and your content metadata. getAds()returning a list, even an empty one, confirms the request reached the endpoint.- 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=3on every request -
bundleread fromroAppInfo.GetID(), not hardcoded -
adunitset to your normal video ad unit name - RAF macros passed literally to
setAdUrl() -
devicetype=3, realwandh,startdelaysent - Content metadata populated from the feed
-
lmtwired through RAF;coppa=1set if child-directed - A store-installed session verified with the real Channel ID