Skip to content

Ads#

Table of Contents#

Introduction#

The Storyteller SDK supports ads created in the Storyteller CMS (First Party Ads), Google Ad Manager (GAM), legacy AdMob, VAST tags, and custom ad modules supplied by an integrator.

Which source of ads is used can be configured on your behalf by a member of the Storyteller Delivery Team.

Choosing an Ad Module#

Choose exactly one native/banner Google Ads product and use the same exact version as com.getstoryteller:sdk:

Product Coordinate Public facade Android floor Google SDK Kotlin build line AdMob
Legacy com.getstoryteller:ads:<storyteller-version> com.storyteller.modules.ads.StorytellerGamModule or StorytellerAdMobModule API 21 Google Mobile Ads 23.4.0 1.9 Supported
GAM v24 com.getstoryteller:ads24:<storyteller-version> com.storyteller.modules.ads.StorytellerGamModule API 23 Google Mobile Ads 24.6.0 2.1 Not included
NextGen GAM com.getstoryteller:adsnextgen:<storyteller-version> com.storyteller.modules.adsnextgen.StorytellerGamModule API 24 Google Mobile Ads NextGen 1.2.1 1.9 or newer Not included
Feature Legacy GAM GAM v24 NextGen GAM Legacy AdMob
Custom Native Templates Supported Supported Supported Not supported
Key-Value Pair Targeting Supported Supported Supported Supported through network extras
Publisher Provided ID Supported Supported Not supported Not applicable
Host request-builder callbacks One AdManagerAdRequest.Builder callback One AdManagerAdRequest.Builder callback Separate native and banner callbacks Not supported
Bottom Banner Ads Supported Supported Supported Supported
Banner Priority Mode Not supported Not supported Not supported Supported

Select exactly one Google Ads product

Do not include ads, ads24, or adsnextgen together, directly or transitively. Their Gradle Module Metadata declares one shared capability, so Gradle rejects incompatible pairs when that metadata is consumed. Maven, Bazel, or Gradle builds that disable gradleMetadata() must enforce the same exactly-one rule in dependency management. Combining products is unsupported and can create duplicate classes; ads and ads24 intentionally expose the same com.storyteller.modules.ads package. Register only one GAM or AdMob facade in Storyteller.modules.

StorytellerGamVastModule is independent of this native/banner choice. It lives in the com.getstoryteller:ads-vast artifact, builds GAM VAST tag URLs, and uses the Storyteller VAST parser and playback pipeline without Google IMA. If your integration uses VAST fallback, follow the module order supplied by the Storyteller Delivery team.

Installing a Google Ads Product#

Ensure Maven Central is available in settings.gradle, then give the core SDK and the selected Ads product one shared version:

dependencyResolutionManagement {
  repositories {
    google()
    mavenCentral()
  }
}
[versions]
storyteller = "<storyteller-version>"

[libraries]
storyteller-sdk = { module = "com.getstoryteller:sdk", version.ref = "storyteller" }
# Keep exactly one of these Google Ads declarations:
storyteller-ads = { module = "com.getstoryteller:ads", version.ref = "storyteller" }
storyteller-ads24 = { module = "com.getstoryteller:ads24", version.ref = "storyteller" }
storyteller-adsnextgen = { module = "com.getstoryteller:adsnextgen", version.ref = "storyteller" }
dependencies {
  implementation(libs.storyteller.sdk)
  implementation(libs.storyteller.ads24) // Or ads / adsnextgen, never more than one.
}

All three products declare the core SDK at the exact product version. Do not override that dependency to a different SDK version; mixed versions are unsupported.

Migrating Between Google Ads Products#

Migration Source changes Required checks
Legacy GAM to GAM v24 Normally dependency-only because the GAM facade and package are retained Remove ads, add ads24 at the exact SDK version, raise the app to API 23 and a Kotlin 2.1-compatible toolchain, then test every GAM placement and bidder callback
Legacy AdMob to GAM v24 Integration rewrite GAM v24 has no StorytellerAdMobModule; replace AdMob configuration with a GAM facade and GAM ad units
Legacy or GAM v24 to NextGen Integration rewrite Remove the old product, add adsnextgen, raise the app to API 24, supply a Google Ads app ID, update the facade import, and migrate builder callbacks to NextGen native/banner types
Any migration back to legacy Product downgrade Restore ads at the exact SDK version and remove APIs that exist only in the selected newer product

After changing products, inspect the resolved runtime dependency graph and verify Stories native ads, Clips native ads, optional bottom banners, targeting, analytics, consent-off behavior, and release/minified builds. Do not use dependency exclusions to bypass the shared-capability conflict.

Storyteller First Party Ads#

If your tenant is configured to use Storyteller First Party Ads, which can be managed in the Storyteller CMS, then no changes to the Storyteller integration code are necessary. The Ads code is managed entirely within the Storyteller SDK.

Clips Opening Pre-roll Timeout#

An opening Clips pre-roll is eligible only when the remote Clips ad cadence initialIndex is explicitly 0 and the presentation has preRollEnabled = true. A missing or null initial index creates neither an opening nor a later full-screen Clips ad slot; a positive initial index keeps its configured later between-Clip cadence without inserting an opening ad.

For an eligible opening request, the SDK uses the CMS-configured timeout, or a 5 second SDK fallback when no CMS timeout is configured. If the ad is not available before the timeout, the player starts the first clip immediately and ignores any late ad callback for that opening placement.

Later between-clips ad requests continue using the normal loading behavior.

Legacy and GAM v24 SDKs#

To use Ads from Google Ad Manager in Storyteller, first reach out to your Storyteller contact and they will assist you with setting up Google Ad Manager to traffic ads to Storyteller.

Use either the legacy ads product or the GAM-only ads24 product described above. Their StorytellerGamModule facade stays in the same package, so the following initialization works for both products. GAM v24 does not include StorytellerAdMobModule.

Basic Setup#

Now initialize the extension as follows:

import com.storyteller.modules.ads.StorytellerGamModule
import com.storyteller.domain.ads.entities.StorytellerAdRequestInfo

val storytellerGamModule = StorytellerGamModule.getInstance(applicationContext).apply {
  init(
    adUnit = { storytellerAdRequestInfo: StorytellerAdRequestInfo -> "/33813572/storyteller" },
  )
}
fun initializeStoryteller() {
  Storyteller.modules = listOf(storytellerGamModule)
  //initialize code
}

You will need to supply the following parameters:

Parameter Name Description
{ storytellerAdRequestInfo -> adUnitId } The lambda which returns desired adUnitId. This can be used for dynamic adUnitId changes depending on storytellerAdRequestInfo content. If you do not need dynamic adUnit changes simply put the static ad unit as a return value.
bottomBannerAdUnit Optional lambda that returns the Ad Unit ID used specifically for the Clips bottom banner placement. Leave this null if you don't plan to serve Clips bottom banner ads.
publisherProvidedId Lambda available on the PPID overload that returns a Google Ad Manager Publisher Provided ID (PPID) for audience targeting. The SDK sends it with setPublisherProvidedId, not as a custom KVP. Return null or a blank value to omit it.
configureAdRequestBuilder Optional callback invoked with the prepared AdManagerAdRequest.Builder before Storyteller builds and loads each native or bottom-banner GAM request. Use this to apply host-owned bidder setup, such as APS, Nimbus, or custom GAM builder mutations.

Then pass the newly created instance of the extension to the modules property on the Storyteller instance:

Storyteller.modules = listOf(storytellerGamModule)

Showcase examples#

Setup with the dynamic Ad Unit changes#

Example for dynamic Ad Unit changes when we want to use different adUnit for Stories and Clips:

import com.storyteller.modules.ads.StorytellerGamModule
import com.storyteller.domain.ads.entities.StorytellerAdRequestInfo

val storytellerGamModule = StorytellerGamModule.getInstance(applicationContext).apply {
  init(
    adUnit = { storytellerAdRequestInfo: StorytellerAdRequestInfo ->
      when (storytellerAdRequestInfo) {
        is StorytellerAdRequestInfo.StoriesAdRequestInfo -> "/33813572/storyteller/stories"
        else -> "/33813572/storyteller/clips"
      }
    },
  )
}

fun initializeStoryteller() {
  Storyteller.modules = listOf(storytellerGamModule)
  //initialize code
}

Setup with the additional parameters#

You can also supply optional parameters templateIds, keyValuePairs, and configureAdRequestBuilder. If you need PPID support, use the init overload that includes publisherProvidedId. The example assumes developerDeviceId, ciamId, and isLoggedIn come from your app's identity and consent state:

import com.google.android.gms.ads.admanager.AdManagerAdRequest
import com.storyteller.modules.ads.StorytellerGamModule
import com.storyteller.domain.ads.entities.StorytellerAdRequestInfo
import com.storyteller.domain.ads.entities.StorytellerCustomNativeTemplateIds

val storytellerGamModule = StorytellerGamModule.getInstance(applicationContext).apply {
  init(
    adUnit = { storytellerAdRequestInfo: StorytellerAdRequestInfo ->
      when (storytellerAdRequestInfo) {
        is StorytellerAdRequestInfo.StoriesAdRequestInfo -> "/33813572/storyteller/stories"
        else -> "/33813572/storyteller/clips"
      }
    },
    templateIds = StorytellerCustomNativeTemplateIds("12102683", "12269089"),
    keyValuePairs = {
      mapOf(
        "ddid" to developerDeviceId,
        "isLoggedIn" to isLoggedIn.toString(),
      )
    },
    publisherProvidedId = { ciamId ?: developerDeviceId },
    configureAdRequestBuilder = { adRequestInfo: StorytellerAdRequestInfo, builder: AdManagerAdRequest.Builder ->
      bidderRequestBuilder(adRequestInfo)?.applyTo(builder)
    },
  )
}

fun initializeStoryteller() {
  Storyteller.modules = listOf(storytellerGamModule)
  //initialize code
}
Parameter Name Description
templateIds If you have worked with the Storyteller Delivery team to setup Custom Native Ads, you will need to supply their IDs here. If you are only using Stories (but not Clips) it is only necessary to supply one property of this struct.
keyValuePairs A function that is called each time we request a new ad. The Storyteller GAM SDK passes a default set of KVPs to GAM to allow targeting based on the content of the Stories/Clips the user is viewing. If you have any additional custom parameters that you need to target by, such as ddid, isLoggedIn, gdpr, us_privacy, gpp, gpp_sid, rdid, idtype, or is_lat, pass them here.
publisherProvidedId A function that is called each time we request a new GAM ad. Return the PPID that should be passed to Google Ad Manager, such as the current user's CIAM ID when logged in or a developer device ID when logged out. The SDK sends this value through AdManagerAdRequest.Builder.setPublisherProvidedId, outside custom KVPs.
configureAdRequestBuilder A function that is called each time the SDK prepares a GAM ad request builder. Storyteller applies its request setup, default st* KVPs, your keyValuePairs, and any publisherProvidedId first, then invokes this callback before calling build() and loading the ad. Use it for host-owned bidder SDKs or custom builder fields that cannot be expressed as KVPs or PPID.

By default, Storyteller always includes the following GAM KVPs:

Request Type Default KVPs
Clips stApiKey (current API key), stCollection (the identifier of the clips collection where the ad will be displayed), stClipCategories (list of categories for the current clip associated with the item for ad targeting), stNextClipCategories (list of categories for the next clip associated with the item for ad targeting), stAdIndex (count of the ad position from the start of the collection).
Stories stApiKey (current API key), stCategories (list of categories for the current story associated with the item for ad targeting), stPlacement (the identifier of the stories placement where the ad will be displayed), stCurrentCategory (list of current story category identifiers used for ad targeting and filtering), stAdIndex (count of the ad position from the start of the placement).

keyValuePairs and publisherProvidedId are only sent when Storyteller.eventTrackingOptions.enableAdTracking is enabled. The SDK does not collect device identifiers, login identifiers, or consent strings automatically; the host app should supply only the values it is permitted to use for ad targeting. If your ad ops setup requires is_lat, normalize it to "0" or "1" before returning it from keyValuePairs.

configureAdRequestBuilder is not automatically privacy-gated by Storyteller. Apply any consent, limited-ad-tracking, or regional privacy checks required by your app before mutating the builder. Storyteller-owned GAM KVP names beginning with st are reserved; do not overwrite or remove them in the callback.

When opening Clips pre-roll is enabled for a presentation, the SDK requests that pre-roll before the first Clip is shown. The opening request uses the first Clip as its content context and sends stAdIndex = 1; existing between-Clip ad cadence continues to use later ad indexes.

Storyteller NextGen GAM SDK#

The adsnextgen product uses Google's Mobile Ads NextGen SDK and is GAM-only. It does not include legacy play-services-ads, play-services-ads-lite, StorytellerAdMobModule, or PPID support. Do not add those legacy Google artifacts alongside it.

NextGen uses the same StorytellerAdRequestInfo and StorytellerCustomNativeTemplateIds domain types as the other products, but its facade package, Google Ads application-ID initialization, and request-builder types are different.

NextGen Basic Setup#

Pass your Google Ads application ID to getInstance, configure the module, and register it before Storyteller.initialize(...):

import com.storyteller.domain.ads.entities.StorytellerAdRequestInfo
import com.storyteller.domain.ads.entities.StorytellerCustomNativeTemplateIds
import com.storyteller.modules.adsnextgen.StorytellerGamModule

val storytellerGamModule = StorytellerGamModule.getInstance(
  applicationContext = applicationContext,
  adAppId = "ca-app-pub-0000000000000000~0000000000",
).apply {
  init(
    adUnit = { requestInfo: StorytellerAdRequestInfo ->
      when (requestInfo) {
        is StorytellerAdRequestInfo.StoriesAdRequestInfo -> "/33813572/storyteller/stories"
        else -> "/33813572/storyteller/clips"
      }
    },
    templateIds = StorytellerCustomNativeTemplateIds("12102683", "12269089"),
    keyValuePairs = { mapOf("appmode" to "prod") },
    bottomBannerAdUnit = { "/33813572/storyteller/clips_banner" },
  )
}

Storyteller.modules = listOf(storytellerGamModule)

The module initializes Google's NextGen SDK once using the supplied application ID. Use the production application ID assigned to your app; Google's sample ID is suitable only for test builds.

NextGen Request-builder Callbacks#

NextGen exposes separate callbacks because Google uses distinct builders for native and banner requests:

import com.google.android.libraries.ads.mobile.sdk.banner.BannerAdRequest
import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdRequest

storytellerGamModule.init(
  adUnit = { "/33813572/storyteller" },
  bottomBannerAdUnit = { "/33813572/storyteller/clips_banner" },
  configureNativeAdRequestBuilder = { requestInfo, builder: NativeAdRequest.Builder ->
    bidderTargeting(requestInfo).forEach { (key, value) ->
      builder.putCustomTargeting(key, value)
    }
  },
  configureBannerAdRequestBuilder = { requestInfo, builder: BannerAdRequest.Builder ->
    bidderTargeting(requestInfo).forEach { (key, value) ->
      builder.putCustomTargeting(key, value)
    }
  },
)

Storyteller applies its request setup, default st* targeting, and keyValuePairs before these callbacks. The callbacks are not automatically privacy-gated: apply host consent rules before adding bidder or targeting data, and do not overwrite Storyteller-owned st* keys.

Storyteller AdMob SDK#

The Storyteller AdMob Module provides integration with standard AdMob ads for apps that do not use Google Ad Manager. This module is an alternative to the GAM module and supports native ads with an optional banner fallback strategy.

Important: AdMob is available only from the legacy ads product. Do not combine it with another GAM facade or Google Ads product.

To use this extension, first install it using Gradle. Ensure Maven Central is available in your settings.gradle:

mavenCentral()

Then add the following reference to your version catalog file:

storyteller-ads = { module = "com.getstoryteller:ads", version.ref = "storyteller" }

And finally reference this in your build.gradle:

implementation(libs.storyteller.ads)

AdMob Basic Setup#

Initialize the AdMob module as follows:

import com.storyteller.modules.ads.StorytellerAdMobModule
import com.storyteller.domain.ads.entities.StorytellerAdRequestInfo

val adMobModule = StorytellerAdMobModule.getInstance(applicationContext).apply {
  init(
    nativeAdUnit = { adRequestInfo: StorytellerAdRequestInfo -> "ca-app-pub-xxx/yyy" },
  )
}

fun initializeStoryteller() {
  Storyteller.modules = listOf(adMobModule)
  // initialize code
}

You will need to supply the following parameters:

Parameter Name Description
nativeAdUnit A lambda function that returns the AdMob ad unit ID for native ads. AdMob ad unit IDs follow the format ca-app-pub-xxx/yyy. This is the primary ad type used by the module.
bannerAdUnit Optional lambda that returns the Ad Unit ID for banner ads used as fallback when native ads fail to load (e.g., no fill).
bottomBannerAdUnit Optional lambda that returns the Ad Unit ID used specifically for the Clips bottom banner placement. Leave this null if you don't plan to serve Clips bottom banner ads.
enableBannerAdPriority Optional boolean. When true, banner ads are attempted first with native ads as fallback. When false (default), native ads are attempted first with banner ads as fallback. Only takes effect if bannerAdUnit is configured.
keyValuePairs Optional lambda that returns a Map<String, String> of custom key-value pairs passed to AdMob as network extras for ad targeting. Only sent when Storyteller.eventTrackingOptions.enableAdTracking is enabled.

Then pass the newly created instance of the extension to the modules property on the Storyteller instance:

Storyteller.modules = listOf(adMobModule)

Example for dynamic Ad Unit changes when you want to use different ad units for Stories and Clips:

import com.storyteller.modules.ads.StorytellerAdMobModule
import com.storyteller.domain.ads.entities.StorytellerAdRequestInfo

val adMobModule = StorytellerAdMobModule.getInstance(applicationContext).apply {
  init(
    nativeAdUnit = { adRequestInfo: StorytellerAdRequestInfo ->
      when (adRequestInfo) {
        is StorytellerAdRequestInfo.StoriesAdRequestInfo -> "ca-app-pub-xxx/stories"
        else -> "ca-app-pub-xxx/clips"
      }
    },
  )
}

fun initializeStoryteller() {
  Storyteller.modules = listOf(adMobModule)
  // initialize code
}

AdMob Setup with Banner Fallback#

The AdMob module supports a banner fallback strategy to maximize fill rate. When configured, if a native ad request fails (e.g., due to no fill), the module automatically attempts to load a banner ad as a fallback:

import com.storyteller.modules.ads.StorytellerAdMobModule
import com.storyteller.domain.ads.entities.StorytellerAdRequestInfo

val adMobModule = StorytellerAdMobModule.getInstance(applicationContext).apply {
  init(
    nativeAdUnit = { adRequestInfo: StorytellerAdRequestInfo -> "ca-app-pub-xxx/native" },
    bannerAdUnit = { adRequestInfo: StorytellerAdRequestInfo -> "ca-app-pub-xxx/banner_fallback" },
  )
}

fun initializeStoryteller() {
  Storyteller.modules = listOf(adMobModule)
  // initialize code
}

The fallback flow works as follows:

  1. The module first attempts to load a native ad using nativeAdUnit
  2. If the native ad fails to load, and bannerAdUnit is configured, it attempts to load a banner ad
  3. If both requests fail, the combined error is reported

AdMob Setup with Bottom Banner Ads#

To enable bottom banner ads in Clips, provide the bottomBannerAdUnit parameter:

import com.storyteller.modules.ads.StorytellerAdMobModule
import com.storyteller.domain.ads.entities.StorytellerAdRequestInfo

val adMobModule = StorytellerAdMobModule.getInstance(applicationContext).apply {
  init(
    nativeAdUnit = { adRequestInfo: StorytellerAdRequestInfo -> "ca-app-pub-xxx/native" },
    bottomBannerAdUnit = { adRequestInfo: StorytellerAdRequestInfo -> "ca-app-pub-xxx/bottom_banner" },
  )
}

fun initializeStoryteller() {
  Storyteller.modules = listOf(adMobModule)
  // initialize code
}

AdMob Setup with Key-Value Pairs#

The AdMob module supports custom key-value pairs for ad targeting. These are passed as network extras to AdMob:

import com.storyteller.modules.ads.StorytellerAdMobModule
import com.storyteller.domain.ads.entities.StorytellerAdRequestInfo

val adMobModule = StorytellerAdMobModule.getInstance(applicationContext).apply {
  init(
    nativeAdUnit = { adRequestInfo: StorytellerAdRequestInfo -> "ca-app-pub-xxx/native" },
    keyValuePairs = { mapOf("customKey" to "customValue", "targeting" to "premium") },
  )
}

fun initializeStoryteller() {
  Storyteller.modules = listOf(adMobModule)
  // initialize code
}

Note: Key-value pairs are only sent when Storyteller.eventTrackingOptions.enableAdTracking is enabled. If ad tracking is disabled, the KVPs will not be included in the ad request.

AdMob Setup with Banner Priority#

By default, the AdMob module attempts to load native ads first, with banner ads as a fallback. You can reverse this priority using enableBannerAdPriority:

import com.storyteller.modules.ads.StorytellerAdMobModule
import com.storyteller.domain.ads.entities.StorytellerAdRequestInfo

val adMobModule = StorytellerAdMobModule.getInstance(applicationContext).apply {
  init(
    nativeAdUnit = { adRequestInfo: StorytellerAdRequestInfo -> "ca-app-pub-xxx/native" },
    bannerAdUnit = { adRequestInfo: StorytellerAdRequestInfo -> "ca-app-pub-xxx/banner" },
    enableBannerAdPriority = true, // Try banner first, native as fallback
  )
}

fun initializeStoryteller() {
  Storyteller.modules = listOf(adMobModule)
  // initialize code
}

When enableBannerAdPriority is true:

  1. The module first attempts to load a banner ad using bannerAdUnit
  2. If the banner ad fails to load, it attempts to load a native ad using nativeAdUnit
  3. If both requests fail, the combined error is reported

Note: enableBannerAdPriority only takes effect when bannerAdUnit is also configured. If bannerAdUnit is not provided, native ads will be loaded regardless of this setting.

AdMob Test Ad Unit IDs#

For development and testing, use Google's official test ad unit IDs to avoid generating invalid impressions:

Ad Type Test Ad Unit ID
Native Advanced ca-app-pub-3940256099942544/2247696110
Banner ca-app-pub-3940256099942544/6300978111

Example setup with test ad units:

val adMobModule = StorytellerAdMobModule.getInstance(applicationContext).apply {
  init(
    nativeAdUnit = { _ -> "ca-app-pub-3940256099942544/2247696110" },
    bannerAdUnit = { _ -> "ca-app-pub-3940256099942544/6300978111" },
  )
}

Note: Replace these test ad unit IDs with your production ad unit IDs before releasing your app.

Key Differences from GAM#

When choosing between StorytellerAdMobModule and StorytellerGamModule, consider the following differences:

Feature StorytellerGamModule StorytellerAdMobModule
Custom Native Templates Supported via templateIds parameter Not supported (AdMob limitation)
Key-Value Pair Targeting Supported via keyValuePairs parameter Supported via keyValuePairs (network extras)
Ad Loading Strategy Unified AdLoader supports multiple formats in single request Native ad first (or banner first with enableBannerAdPriority), then fallback
Banner Priority Mode Not supported Supported via enableBannerAdPriority
Bottom Banner Ads Supported Supported
Ad Unit Format GAM format: /network/ad_unit AdMob format: ca-app-pub-xxx/yyy

When to use AdMob Module:

  • Your app uses standard AdMob and does not have a Google Ad Manager account
  • You do not need custom native ad templates
  • You want configurable ad loading priority (banner vs native first)
  • You want a simpler integration with automatic banner fallback

When to use GAM Module:

  • You have a Google Ad Manager account
  • You need custom native ad templates configured with the Storyteller Delivery team
  • You need unified ad loading with multiple formats in a single request
  • You need the default set of Storyteller KVPs automatically included (e.g., stApiKey, stCollection, stClipCategories)

Storyteller VAST SDK#

The Storyteller VAST module loads VAST linear video ads through the same Storyteller.modules ad-module system. It is generic VAST support, not an ITV-specific integration. ITV is the first validated customer scenario, but any compatible VAST server can be used when the Storyteller Delivery team has configured your tenant for VAST ads.

VAST Setup#

Add the Storyteller:ads-vast artifact alongside the Storyteller Maven repository:

storyteller-ads-vast = { module = "Storyteller:ads-vast", version.ref = "storyteller" }
implementation(libs.storyteller.ads.vast)

Initialize the module with your VAST endpoint and request parameters:

import com.storyteller.modules.vast.StorytellerVastModule
import com.storyteller.modules.vast.UrlFormat

val vastModule = StorytellerVastModule.getInstance(applicationContext).apply {
  init(
    baseUrl = "https://example.com/vast",
    requestParameters = { adRequestInfo ->
      mapOf(
        "placementId" to "home-preroll",
        "adIndex" to "0",
      )
    },
    urlFormat = UrlFormat.QueryString,
  )
}

fun initializeStoryteller() {
  Storyteller.modules = listOf(vastModule)
  // initialize code
}

The requestParameters callback receives the current ad request context for per-placement mappings. These examples use static values for clarity; replace them with the request parameters required by your VAST server.

Verbose VAST info/debug logging is disabled by default. During local QA, pass enableDebugLogging = true to StorytellerVastModule.getInstance(...) or StorytellerGamVastModule.getInstance(...); this process-wide toggle is shared by all VAST module instances. Later calls to getInstance(...) without enableDebugLogging leave the current setting unchanged. Warning and error log lines are always emitted regardless of this flag.

For fallback scenarios, module order controls priority. For example, Storyteller.modules = listOf(vastModule, gamModule) asks VAST first, then GAM if VAST does not fill. Follow the module order provided by the Storyteller Delivery team for your tenant.

GAM VAST Setup#

Use StorytellerGamVastModule when Storyteller Delivery provides a Google Ad Manager VAST setup. The module always uses the GAM VAST endpoint and query-string URL format, then delegates the response into the same VAST loading, parsing, tracking, wrapper, media-selection, and playback payload pipeline used by StorytellerVastModule.

import com.storyteller.modules.vast.StorytellerGamVastModule

val gamVastModule = StorytellerGamVastModule.getInstance(applicationContext).apply {
  init(
    adUnit = { "/1234567/vast_video" },
    descriptionUrl = { "https://publisher.example/story" },
    contentUrl = { "https://publisher.example/content" },
    customParams = {
      mapOf(
        "content_category" to "sports",
      )
    },
    tagParameters = { emptyMap() },
  )
}

fun initializeStoryteller() {
  Storyteller.modules = listOf(gamVastModule)
  // initialize code
}

StorytellerGamVastModule automatically adds SDK-owned GAM VAST defaults including output, env, gdfp_req, vpa, correlator, vpmute, and sz when reliable. The SDK uses the measured Activity window size for sz when available, then falls back to Android display metrics. customParams are serialized into the GAM cust_params query parameter. tagParameters are serialized as top-level query parameters and are merged last, so host apps can add tenant-specific values or override any SDK-set top-level value by supplying the same key. For SDK-owned parameter ownership and override behavior, see VAST and GAM VAST Parameter Tables.

GAM VAST Parameter Mapping#

Callback GAM parameter Notes
adUnit iu Required GAM ad unit path.
descriptionUrl description_url Required page or content description URL.
contentUrl url Optional content URL; blank values are omitted.
customParams cust_params Serialized as nested key-value pairs, for example content_category=sports.
tagParameters Top-level query parameters Applied last. Use this for tenant-specific values or intentional overrides of SDK-owned GAM values such as output, env, gdfp_req, vpa, sz, correlator, or vpmute, and SDK-owned bid defaults such as adtype, vw, vh, or vplay.

Serialization examples:

Input Serialized output
customParams = mapOf("content_category" to "sports") cust_params=content_category%3Dsports
customParams = mapOf("section" to "news&sport") cust_params=section%3Dnews%2526sport
tagParameters = mapOf("sz" to "1080x1920") Overrides the SDK-calculated sz value.
tagParameters = mapOf("cust_params" to "client_override=1") Overrides the value produced from customParams.

VAST and GAM VAST Parameter Tables#

VAST setups can combine SDK-owned bid defaults with tenant-specific generic VAST request parameters and GAM VAST tag parameters. Storyteller Delivery will confirm the final values for each tenant. The SDK applies bid defaults before client overrides: generic StorytellerVastModule.requestParameters can override the generic VAST top-level values, and StorytellerGamVastModule.tagParameters can override the same top-level values for GAM VAST.

Parameter Applies to Set by Override path
iu GAM VAST adUnit callback tagParameters["iu"]
description_url GAM VAST descriptionUrl callback tagParameters["description_url"]
url GAM VAST contentUrl callback when non-blank tagParameters["url"]
cust_params GAM VAST Encoded customParams map tagParameters["cust_params"] replaces the encoded map
content_category GAM VAST custom targeting customParams["content_category"] Supply a different customParams value, or override all cust_params through tagParameters["cust_params"]

The SDK-owned GAM VAST value parameters are top-level GAM tag parameters. StorytellerGamVastModule sets these automatically before host tagParameters are merged, and hosts can override any top-level value by supplying the same key in tagParameters.

Parameter Value Source Type Meaning Override path
output vast SDK constant Static Request VAST XML response tagParameters["output"]
env vp SDK constant Static Video player environment tagParameters["env"]
gdfp_req 1 SDK constant Static GAM ad request marker tagParameters["gdfp_req"]
sz <width>x<height>, for example 1080x1920; omitted if unavailable Measured Activity window size, falling back to Android display metrics Dynamic per request Ad slot size in physical pixels tagParameters["sz"]
correlator Generated numeric value SDK request counter Dynamic per request Request cache-busting tagParameters["correlator"]
vpa auto SDK constant Static Video playback automatic tagParameters["vpa"]
vpmute 1 when muted, 0 when unmuted Current AdContext.isMuted Dynamic per request Player mute state tagParameters["vpmute"]

The SDK sets these bid defaults on both generic VAST and GAM VAST requests:

Parameter SDK value or source Static/dynamic Meaning Override path
adtype 13 Static Ad type Generic requestParameters["adtype"]; GAM tagParameters["adtype"]
plcmt 3 Static Placement type Generic requestParameters["plcmt"]; GAM tagParameters["plcmt"]
vw Current Android window or screen width in physical pixels Dynamic Viewport width Generic requestParameters["vw"]; GAM tagParameters["vw"]
vh Current Android window or screen height in physical pixels Dynamic Viewport height Generic requestParameters["vh"]; GAM tagParameters["vh"]
vminl 5 Static Minimum video duration Generic requestParameters["vminl"]; GAM tagParameters["vminl"]
vmaxl 30 Static Maximum video duration Generic requestParameters["vmaxl"]; GAM tagParameters["vmaxl"]
vfmt 1+5 Static Accepted video formats Generic requestParameters["vfmt"]; GAM tagParameters["vfmt"]
vadFmt 2+3+8 Static Accepted ad formats Generic requestParameters["vadFmt"]; GAM tagParameters["vadFmt"]
vplay 6 for muted autoplay, 5 for sound-on autoplay Dynamic Playback mode Generic requestParameters["vplay"]; GAM tagParameters["vplay"]
vskip 1 Static Skippable ad signal Generic requestParameters["vskip"]; GAM tagParameters["vskip"]
vpos 0 Static Position signal Generic requestParameters["vpos"]; GAM tagParameters["vpos"]
vcom 0 Static Companion ad signal Generic requestParameters["vcom"]; GAM tagParameters["vcom"]
vcont 1 Static Content type signal Generic requestParameters["vcont"]; GAM tagParameters["vcont"]
vtype 1 Static Video type signal Generic requestParameters["vtype"]; GAM tagParameters["vtype"]
vminbtr 600 Static Minimum bitrate in Kbps Generic requestParameters["vminbtr"]; GAM tagParameters["vminbtr"]
vmaxbtr 8000 Static Maximum bitrate in Kbps Generic requestParameters["vmaxbtr"]; GAM tagParameters["vmaxbtr"]

VAST URL Formats#

UrlFormat controls how the requestParameters map is serialized into the final VAST tag URL:

Format Output shape Use when
UrlFormat.PathSegment https://example.com/vast/k1=v1/k2=v2 Your VAST server expects key-value pairs as path segments.
UrlFormat.QueryString https://example.com/vast?k1=v1&k2=v2 Your VAST server expects standard query parameters.

Path-segment example:

val vastModule = StorytellerVastModule.getInstance(applicationContext).apply {
  init(
    baseUrl = "https://example.com/vast",
    requestParameters = { adRequestInfo ->
      mapOf(
        "slot" to "video-preroll",
        "placement" to "home",
        "adIndex" to "0",
      )
    },
    urlFormat = UrlFormat.PathSegment,
  )
}

Query-string example:

val vastModule = StorytellerVastModule.getInstance(applicationContext).apply {
  init(
    baseUrl = "https://example.com/vast",
    requestParameters = { adRequestInfo ->
      mapOf(
        "placementId" to "home-preroll",
        "placement" to "home",
        "adIndex" to "0",
      )
    },
    urlFormat = UrlFormat.QueryString,
  )
}

The module validates that the final VAST URL is HTTPS and no longer than 2,048 characters. Parameter names and values are owned by your VAST server configuration.

VAST Supported Scope#

The first public release supports:

  • Inline linear video ads.
  • Compatible VAST 2.0, 3.0, 4.0, 4.1, and 4.2 parsing.
  • Wrapper resolution with depth and loop protection.
  • Media file selection for playable video creatives.
  • Click-through and click tracking.
  • Impression and playback tracking.
  • Skip tracking.
  • Deferred VAST error handling when another module may still fill the same ad slot.
  • Static-resource VAST Icon overlay rendering for Stories and Clips ad video players.
  • Parser-only caption capture for future SDK work.

The following VAST features are deferred or out of scope for v1:

  • Nonlinear ads.
  • Companion ads.
  • OMID or verification rendering.
  • VPAID execution.
  • Server-side ad insertion behavior.
  • VAST Icon IframeResource and HTMLResource rendering.
  • Ad caption rendering in v1.
  • Bottom banner ads through the VAST module.

VAST Tracking and Skip Behavior#

VAST impression, playback, click, skip, and supported Icon tracking URLs are mapped into Storyteller ad tracking flows. Multiple URLs for the same VAST event are preserved and dispatched for that event.

VAST Icon Overlay Rendering#

VAST <Icon> elements are treated as industry, privacy, or program overlays such as AdChoices badges. They are rendered over the VAST ad video and are never used as the advertiser logo or CTA.

The SDK renders one compatible AdChoices Icon per VAST ad, including Icons declared on resolved Wrappers. Compatible Icons must provide program="AdChoices" and a non-empty StaticResource image URL. Non-AdChoices Icons, IframeResource Icons, and HTMLResource Icons are ignored by the renderer.

The selected Icon renders inside the ad video frame for Stories and Clips. The SDK honors VAST xPosition values of left, right, or a non-negative pixel offset, and yPosition values of top, bottom, or a non-negative pixel offset. Missing or unsupported position values fall back to top-right. offset and duration are honored when present: the Icon is hidden before its offset and hidden again after its duration window. Without timing attributes, the Icon is visible for the ad lifetime.

When the Icon image first loads and becomes visible, every IconViewTracking URL fires once. When the visible Icon is tapped, every IconClickTracking URL fires. If IconClickThrough is present, the SDK opens it in the normal external browser flow after firing click tracking. Icon taps do not fire the main ad CTA click tracking path. If the StaticResource image cannot be loaded, the Icon is not rendered, has no tappable hitbox, and does not fire view or click tracking.

Linear@skipoffset is parsed when present. When backend non-skippable ads are enabled, a valid VAST skipoffset becomes the ad-specific non-skippable duration for that VAST ad. If a VAST ad omits skipoffset, or the value cannot be resolved, the SDK continues to use the backend nonSkippableAdsConfig.duration fallback.

VAST <Error> URLs are deferred while the SDK tries the configured fallback chain. If a downstream module fills the same ad slot, the queued VAST error URLs are discarded. If no module fills the slot, the queued VAST error URLs fire when the slot reaches its intended playhead. If the slot is cancelled before it reaches the intended playhead, queued VAST error URLs are discarded.

ITV as the First Validated Scenario#

ITV is the first validated integration scenario for Android VAST. It uses a path-segment URL shape and tenant-provided request parameters, but the SDK does not hard-code ITV endpoints, campaign fields, tracking names, or metadata.

For ITV-style integrations, Storyteller Delivery supplies the endpoint, the required parameter names, and the module order. Host apps should pass those values through StorytellerVastModule.init(...) exactly as provided.

VAST Branded Presentation Metadata#

VAST ads can provide Storyteller presentation metadata through Extension type="storyteller:ad-ui". The SDK reads CtaText and AdvertiserName from the StorytellerAdUi child element:

<Extension type="storyteller:ad-ui">
  <StorytellerAdUi>
    <CtaText>Shop now</CtaText>
    <AdvertiserName>Example Brand</AdvertiserName>
  </StorytellerAdUi>
</Extension>

The SDK resolves the final presentation values with this priority:

Field Resolution priority
CTA text VAST extension CtaText -> backend default ads.presentationDefaults.vast.ctaText -> VAST <AdTitle> -> VAST <Advertiser> -> Learn more
Advertiser display name VAST extension AdvertiserName -> backend default ads.presentationDefaults.vast.advertiserName -> VAST <Advertiser> -> VAST <AdTitle>

CTA text is only shown when the VAST ad has a non-empty <ClickThrough> destination. VAST <Icon> elements are reserved for industry, privacy, or program overlays and are not used as brand logos.

VAST QA Checklist#

Before enabling a tenant for VAST, validate:

  • The app resolves Storyteller:ads-vast:<version> from the Storyteller Maven repository.
  • StorytellerVastModule is initialized before Storyteller.initialize(...).
  • StorytellerGamVastModule is initialized before Storyteller.initialize(...) when the tenant uses a GAM VAST setup.
  • The configured URL format matches the VAST server contract.
  • GAM VAST customParams and tagParameters serialize into the expected GAM tag URL, and tagParameters overrides are intentional.
  • If the tenant uses path-segment URLs, such as an ITV-style setup, that configuration works end to end.
  • A generic query-string VAST configuration works against a query-string VAST test server when the tenant uses query-string URL format.
  • Inline video ad playback reaches impression, quartile, complete, click, and skip tracking paths.
  • Multiple tracking URLs for the same event are preserved.
  • Fallback behavior is correct when VAST returns no ad, malformed XML, no compatible media, or wrapper failures.
  • VAST skipoffset behavior matches the tenant's non-skippable ad configuration.

Bottom Banner Ads#

The Clips Player supports bottom banner ads rendered as standard banner views displayed below the video content. Both the GAM module and AdMob module support bottom banner ads. Supply bottomBannerAdUnit in the init() method to fetch inline adaptive banners for the Clips bottom banner placement. Only 300x50 and 320x50 ad sizes are supported. For GAM, configureAdRequestBuilder is applied to bottom banner requests as well as native ad requests.

Example setup with bottom banner ads using GAM:

val storytellerGamModule = StorytellerGamModule.getInstance(applicationContext).apply {
  init(
    adUnit = { storytellerAdRequestInfo: StorytellerAdRequestInfo -> "/your/ad_unit_id" },
    bottomBannerAdUnit = { storytellerAdRequestInfo: StorytellerAdRequestInfo -> "/your/bottom_banner_ad_unit_id" },
  )
}

Example setup with bottom banner ads using AdMob:

val adMobModule = StorytellerAdMobModule.getInstance(applicationContext).apply {
  init(
    nativeAdUnit = { adRequestInfo: StorytellerAdRequestInfo -> "ca-app-pub-xxx/native" },
    bottomBannerAdUnit = { adRequestInfo: StorytellerAdRequestInfo -> "ca-app-pub-xxx/bottom_banner" },
  )
}

Ad Source for Custom Modules#

If your custom StorytellerModule provides Google ads, set googleAdSource to declare the source used by your implementation.

  • Use StorytellerGoogleAdSource.GAM for Google Ad Manager modules.
  • Use StorytellerGoogleAdSource.ADMOB for Google AdMob modules.
  • Use null for non-Google ad providers.

Setting googleAdSource enables consistent Google paid ad analytics source values and allows the SDK to normalize multiple Google modules safely.

StorytellerGamModule and StorytellerAdMobModule set googleAdSource automatically.

Ad Request Information#

StorytellerAdRequestInfo#

The StorytellerAdRequestInfo sealed class has two subclasses:

ClipsAdRequestInfo

Used when an Ad is requested for display in a Clips Player. The properties include:

  • collection - The identifier of the clips collection where the ad will be displayed
  • nextClipCategories - List of categories for the next clip associated with the item for ad targeting
  • adIndex - Count of the Ad position from the start of the Collection
  • itemInfo - Metadata about the item context for ad targeting

For a Clips opening pre-roll configured at zero index, adIndex is 1 and itemInfo describes the first Clip that will be shown after the pre-roll.

StoriesAdRequestInfo

Used when an Ad is requested for display in a Stories Player. The properties include:

  • placement - The identifier of the stories placement where the ad will be displayed
  • categories - List of category identifiers for ad targeting and filtering
  • adIndex - Count of the Ad position from the start of the Collection
  • itemInfo - Metadata about the item context for ad targeting

ItemInfo#

Each request class includes an ItemInfo object that contains:

  • categories - List of categories associated with the item for ad targeting

Non Skippable Ads#

Our Player can enforce a period of time during which ads can't be skipped. When enabled, user interactions that would skip a Story or Clip Ad won't be allowed for that duration. This feature can be configured in the CMS.