# Roku (RAF)

Source: https://docs.ezoic.com/docs/ctv/roku/


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)](/docs/ctv/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](/docs/ctv/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

```brightscript
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

