The Storyteller SDK supports ads created in the Storyteller CMS (First Party Ads), Google Ad Manager (GAM), AdMob, VAST tags, true Google IMA pre-roll for Clips, 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.
Choose exactly one native/banner Google Ads product and use the same exact version as com.getstoryteller:sdk:
The table below describes the 11.7.0 release line. On the separate 12.0.0 line, all Storyteller products use Kotlin 2.1+; the core SDK, Legacy Ads, VAST, and IMA require API 23, while GAM v24 remains API 23 and NextGen remains API 24. See Choosing the SDK Release Line before upgrading.
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 or StorytellerAdMobModule
Separate sync or async native and banner callbacks
Not supported
Bottom Banner Ads
Supported
Supported
Supported
Supported
Banner Priority Mode
Not supported
Not supported
Not supported
Supported
Swipeable Interstitial Clips
Not supported
Not supported
Supported with GAM activation
Not 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. The registered module order remains the default fallback order. A Clips presentation can override the eligible standard providers and their order with betweenClipsAdProviderOrder.
StorytellerImaModule is also independent of the native/banner product capability. It lives in com.getstoryteller:ads-ima and can be registered alongside Legacy ads, GAM v24 ads24, or adsnextgen. The IMA artifact uses the Media3 IMA integration already supplied by the core SDK, so host apps do not add a separate IMA dependency.
[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"}# Optional Clips-only IMA pre-roll, compatible with any one selected product:storyteller-ads-ima={module="com.getstoryteller:ads-ima",version.ref="storyteller"}
dependencies{implementation(libs.storyteller.sdk)implementation(libs.storyteller.ads24)// Or ads / adsnextgen, never more than one.implementation(libs.storyteller.ads.ima)// Optional true Clips pre-roll.}
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.
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
Normally dependency-only because the AdMob 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 AdMob placement
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.
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.
A standard native opening ad-as-Clip is eligible only when the effective Clips initialIndex is 0. The effective value comes from a valid per-presentation override when supplied, otherwise from remote Clips cadence. A missing or null effective initial index never creates an opening ad; it uses frequency-only later cadence, with slots after frequency, 2 * frequency, and subsequent content Clips. A positive effective initial index keeps its configured later between-Clip cadence without inserting an opening ad. Embedded Clips continue to suppress the standard zero-index opening placement while retaining later cadence.
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.
When a correctly configured StorytellerImaModule is registered and preRollEnabled is true, the SDK instead attempts one true player-level IMA pre-roll before the initially opened Clip, regardless of its index. IMA suppresses only the standard zero-index opening ad-as-Clip. Later native ad cadence and bottom banners are unchanged.
Register every provider that a presentation may select. The per-presentation order overrides the standard provider order for that request; it does not reorder or mutate the registered module list:
bottomBannerEnabled defaults to true and controls only the bottom-banner placement. Embedded Clips still treat an omitted adConfiguration as bottom-banner disabled.
preRollEnabled defaults to true. Set it to false to suppress both an IMA opening pre-roll and a standard zero-index opening ad-as-Clip for this presentation. It does not disable later remotely scheduled between-Clip ads or bottom banners. The value is fixed for the active opening session; changing it later affects the next presentation.
betweenClipsAdProviderOrder = null preserves existing behavior: all registered standard/custom modules are attempted in their process-wide order.
A populated betweenClipsAdProviderOrder is both an order and an allowlist. The SDK attempts only matching recognized providers, in the supplied order. VAST selects StorytellerVastModule or StorytellerGamVastModule; GAM selects the registered Legacy, GAM v24, or NextGen GAM module; ADMOB selects the registered AdMob module; SWIPEABLE_INTERSTITIAL selects the independently configured NextGen GAM swipeable route.
betweenClipsAdProviderOrder = emptyList() disables standard between-Clip ad requests for this presentation. IMA remains independently controlled by preRollEnabled, and bottom banners remain independently controlled by bottomBannerEnabled.
Duplicate provider values use their first occurrence. An unavailable provider is skipped safely. If multiple modules advertise the same provider, their relative registered order is retained. Modules with no recognized provider capability are excluded by an explicit allowlist but remain available when the value is null.
frequency = null inherits the remote between-Clip frequency. A positive value overrides it for this presentation.
initialIndex = null inherits the remote initial index. Zero is the special opening-ad value. Positive values are one-based content positions, so 2 schedules the first eligible slot after the second Clip.
The two cadence fields resolve independently. An invalid frequency (0 or negative) or initialIndex (negative) logs a value-redacted warning and inherits only that remote field.
Cadence is fixed when the player opens. Changing a configuration object later affects only a new presentation and never mutates process-wide Settings.
The remote Clips strategy and ad source remain authoritative. Cadence overrides apply only when remote settings select the eligible between-Clips strategy; they cannot enable ads for an ineligible strategy or source. Consent, registered modules, provider availability, request timeout, no-fill, and player eligibility continue to apply.
Provider attempts are sequential. For example, VAST-first waits for the VAST attempt to fill, fail, or time out before GAM starts. Swipeable and standard GAM attempts also remain independently orderable even though both use the same registered NextGen module. Every fallback attempt keeps the same slot and adRequestId. Choose the preferred route first only when its possible load delay is acceptable.
Use named arguments in new integrations. For binary and source compatibility, the existing positional StorytellerClipsAdConfiguration(false) form still means bottomBannerEnabled = false; it does not disable pre-roll.
Add the version-aligned IMA artifact alongside the core SDK and exactly one Legacy, GAM v24, or NextGen Ads product:
dependencies{implementation("com.getstoryteller:sdk:<storyteller-version>")implementation("com.getstoryteller:ads24:<storyteller-version>")// Or ads / adsnextgen.implementation("com.getstoryteller:ads-ima:<storyteller-version>")}
Do not add androidx.media3:media3-exoplayer-ima or Google IMA separately. The core Storyteller SDK already provides the compatible Media3 IMA runtime. ads-ima supports API 21 and the same Kotlin 1.9 build line as the core SDK.
The callbacks receive StorytellerAdRequestInfo.ClipsAdRequestInfo for the opening opportunity. customParams are encoded into GAM cust_params. The SDK also adds the same VAST bid defaults used by generic VAST and GAM VAST requests, including adtype=13, plcmt=3, physical-pixel vw/vh, and mute-aware vplay. tagParameters are merged last as top-level parameters and may intentionally override any SDK-generated value. These defaults apply only when Storyteller constructs the tag through initWithGam; complete tags supplied through initWithAdTagUrl are used unchanged.
The static String overload can be used when one complete tag applies to every opening. Generated tags must be valid HTTPS URLs no longer than 8,192 characters. Optional positive vastLoadTimeoutMs and mediaLoadTimeoutMs values are forwarded to Media3 IMA. IMA's native phase timeouts remain authoritative. The SDK arms a 60-second fail-open safety watchdog as soon as it claims the opening opportunity while waiting for the transient player context. Player playback gates such as first-use instructions do not defer context creation; the IMA source is prepared while playback remains paused. Once the IMA request/session is created, that timer is replaced by a watchdog that waits at least 60 seconds, or the configured VAST plus media timeout budgets plus 2 seconds when that total is longer.
IMA runs once per Clips opening session, before whichever Clip was initially opened through a tile, openCollection, deep link, or Embedded Clips.
A single-Clip action presentation is a supported tile/programmatic opening and receives the same one opening opportunity.
IMA is eligible only for an initial Default or For You root Clips opening. Direct Following, Clips search, category-feed, initial-category, and category-navigation routes do not request or play an IMA pre-roll.
It is supported on Android phones and tablets. Stories and Android TV are not supported.
It does not provide IMA interstitials, mid-rolls, repeated cadence, or banners.
Invalid configuration, no-fill, IMA or player-side ad playback errors, or timeout hand content to the initially opened Clip once. The SDK does not fall back to a standard zero-index opening ad for that session.
If a shared feed/cache update removes or reorders the Clip bound to an opening before content handoff, the SDK releases the IMA session and fails open instead of leaving playback or navigation blocked.
Rotation, foreground/background transitions, and ad click-through return do not replay the opening ad. Closing the player cancels the session.
A paused Embedded Clips opening prepares and retains its IMA request/session as soon as the route is active, while ExoPlayer still honors the host playback pause. This prevents another Clip from bypassing the opening pre-roll and ensures the fail-open watchdog is bounded even when host playback remains disabled.
From opening claim through IMA handoff, content navigation and interactions remain blocked. A skippable IMA ad unlocks navigation after skip hands playback to content; a non-skippable IMA ad unlocks it after completion hands playback to content.
Storyteller emits its normal ad analytics only for lifecycle events actually reported by IMA; tag URLs and raw IMA error messages are not included. Pod dismissal analytics bind to the currently started creative, mid-ad errors close that creative with Finished Ad, and IMA playback time is excluded from Clip viewing duration.
StorytellerClipsAdConfiguration.preRollEnabled is the per-presentation eligibility control. A correctly configured IMA module is still required to serve IMA, but the SDK does not request or claim IMA when this value is false. The same value also suppresses the standard zero-index opening ad-as-Clip. It does not affect later cadence or bottom banners.
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 v24 ads24 product described above. Their StorytellerGamModule facade stays in the same package, so the following initialization works for both products. Both products also expose StorytellerAdMobModule; register only one facade.
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 synchronous or asynchronous 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:
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:
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. The two-argument callback mutates the builder synchronously; the three-argument overload waits for explicit completion 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.
Use the three-argument configureAdRequestBuilder overload when a bidder SDK completes later. The following is conceptual—replace bidder and BidderResult with your bidder SDK's API:
Storyteller waits to build and load that request until the callback completes. Call exactly one terminal method:
Completion
Behavior
onSuccess(builder)
Continues with the supplied prepared or replacement builder. Storyteller reapplies its reserved targeting, client KVPs, and PPID before load while preserving other host AdMobAdapter network extras. Storyteller-owned keys take precedence on collision.
onNoFill()
Continues with a clean Storyteller request without bidder enrichment, discarding any partial mutations made before no-fill.
onFailure()
Continues with a clean Storyteller request without bidder enrichment, discarding any partial mutations made before failure.
The first terminal call wins; duplicate or late completions are ignored. If the callback throws before completing, Storyteller treats it as onFailure(). If the Storyteller ad context is cancelled while enrichment is pending, the request is not loaded and later completion is ignored.
The host owns bidder timeout policy. Every configured asynchronous callback must eventually call one terminal method unless the ad context is cancelled. The existing two-argument callback remains synchronous and source/binary compatible.
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.
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.
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 GAM can render Google's swipeable-interstitial creative as an exact native view between Clips. Configure its ad unit independently of init(...), then include SWIPEABLE_INTERSTITIAL in the presentation's provider order:
storytellerGamModule.setClipsSwipeableInterstitialAdUnit{requestInfo->// Resolve the swipeable unit for this Clips request."/1234567/clips/swipeable-interstitial"}valadConfiguration=Storyteller.StorytellerClipsAdConfiguration(betweenClipsAdProviderOrder=listOf(Storyteller.StorytellerAdProvider.SWIPEABLE_INTERSTITIAL,Storyteller.StorytellerAdProvider.GAM,),)
The setter composes with every synchronous and asynchronous init(...) overload. Pass null to disable the route. Returning null or blank for one request makes that attempt fail immediately and continue to the next configured provider. Swipeable requests receive the same consent-gated Storyteller and client key-value targeting as standard NextGen GAM requests.
SWIPEABLE_INTERSTITIAL applies only to later between-Clip slots in direct and Embedded Clips. It is never used for Stories, the standard zero-index opening ad, IMA pre-roll, or bottom banners. GAM continues to select the standard NextGen native/banner route, so either route can precede the other without registering a second module. A Swipeable-only provider list is also supported.
Storyteller passes the exact view returned by Google's SwipeableInterstitialAd.getView(Activity) into the Clips renderer and destroys the Google ad when the SDK ad view is disposed. Google owns taps and horizontal creative interactions; vertical drags belong to Clips paging and do not scroll the ad document. Storyteller does not request Google's optional screen-hold timer or custom click gestures, so the pinned Google defaults remain zero seconds and disabled. Storyteller treats the page as immediately skippable interactive content rather than video playback and does not add a second countdown. A request that does not complete within 10 seconds fails through to the next provider, and any later Google result is destroyed.
On phones and tablets, Storyteller requests a canvas based on the current window's safe content area in dp, excluding system bars and display cutouts. The complete ad surface is proportionally fitted into the actual Clips page, including when an Embedded host reserves space for bottom navigation. The 320x480 value below describes Google's reference creative rather than a fixed SDK request size.
When a tablet changes orientation, Storyteller removes the visible Swipeable ad and shows the next available Clip without an animated scroll or advertising loader. If there is no following Clip, it returns to the previous available Clip. The removed ad placement is not requested again during that feed session. If a regular Clip is visible, it stays selected. Future Swipeable placements are reloaded in the background using the settled dimensions and are skipped if the viewer passes them before an ad is ready. Other changes to request-canvas dimensions, density, or Activity identity still replace incompatible creatives after layout settles.
Before a Swipeable ad enters the feed, Storyteller measures its content and prepares its native viewport while hidden. The complete measured surface is uniformly scaled, top aligned, and horizontally centered within the safe player area. This behavior applies to both debug and release apps without additional configuration. There are no fixed 10% margins, and the SDK does not rewrite creative HTML. Preparation preserves Google View identities and adjusts native viewport dimensions before display; a creative that cannot be prepared within the bounded readiness and resize limits is discarded and the configured provider fallback continues. Resizing the available area without changing the safe-window request canvas updates only the outer fit. A response exceeding its requested canvas is rejected and follows the configured provider fallback.
Host apps do not need to supply sizes or change their integration. Ordinary Clips and other ad formats retain their existing layout policies. System and explicit host insets are respected. Only on landscape tablets with For You / Following tabs enabled, Swipeable reserves 48 dp below them in addition to the host top inset and fits within the remaining height. Phones and portrait tablets have no additional header gap.
GAM activation and creative requirements
Swipeable interstitial availability and trafficking are controlled by Google and your GAM account. Coordinate activation with your Google account team and Storyteller Delivery before production use. Traffic a compatible swipeable-interstitial creative to the configured unit; Google's reference format uses a 320x480 creative. Keep the standard GAM route configured as fallback until production delivery is verified.
Analytics retain adSource = "gam" because both routes use GAM. Swipeable request, failure, loaded, paid, and rendered lifecycle events use adFormat = "swipeableInterstitial"; fallback attempts share the same adRequestId, and the winning route owns later lifecycle attribution.
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.
For asynchronous bidder work, use the equivalent three-argument callbacks:
The completion, cancellation, exception, duplicate-call, and host-owned timeout semantics are the same as Legacy/GAM v24. When onSuccess supplies a replacement NextGen builder, construct it for the same ad unit and requested ad types or ad sizes as the prepared builder. Storyteller reapplies the mutable request setup it owns, including reserved targeting, client KVPs, ad size/options, and custom-template IDs.
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 from either the legacy ads product or the GAM v24 ads24 product in 11.5.1 and 11.6.1 or newer. Version 11.6.0 omitted StorytellerAdMobModule from ads24; upgrade to 11.6.1 or newer instead of adapting application code. Select exactly one Google Ads product and register only one GAM or AdMob facade.
To use this extension, first install it using Gradle. Ensure Maven Central is available in your settings.gradle:
mavenCentral()
Then add exactly one of the following references to your version catalog file:
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:
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:
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.
By default, the AdMob module attempts to load native ads first, with banner ads as a fallback. You can reverse this priority using enableBannerAdPriority:
importcom.storyteller.modules.ads.StorytellerAdMobModuleimportcom.storyteller.domain.ads.entities.StorytellerAdRequestInfovaladMobModule=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)}funinitializeStoryteller(){Storyteller.modules=listOf(adMobModule)// initialize code}
When enableBannerAdPriority is true:
The module first attempts to load a banner ad using bannerAdUnit
If the banner ad fails to load, it attempts to load a native ad using nativeAdUnit
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.
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.
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, registered module order controls the default priority. For example, Storyteller.modules = listOf(vastModule, gamModule) asks VAST first, then GAM if VAST does not fill. A Clips presentation with a non-null betweenClipsAdProviderOrder overrides that standard-provider order for its own requests; other presentations and Stories retain the registered order. Follow the module and presentation configuration provided by the Storyteller Delivery team for your tenant.
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.
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.
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")
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 generic VAST, GAM VAST, and IMA GAM-generated requests. For IMA GAM-generated tags, use the same keys in tagParameters to override them:
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 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> 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 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 ads can provide Storyteller presentation metadata through Extension type="storyteller:ad-ui". The SDK reads CtaText and AdvertiserName from the StorytellerAdUi child element:
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.
The Clips Player supports bottom banner ads rendered as standard banner views in the lower player
region. With a banner and effective BOTTOM progress placement, Embedded Clips use a compact 8dp
progress region matching the track's maximum visual height, without a larger scrub-target reservation.
When width-constrained video creates enough measured bottom letterbox for the banner and its 16dp
lower band, the visible track uses the region's top edge at a fixed 16dp before the banner. The banner
remains 16dp from the local host boundary; extra letterbox height does not increase the track-to-banner
gap. Without qualifying letterbox space, preserve the existing banner-before-progress arrangement:
the banner remains 8dp above the compact progress region, and its bottom-edge track is 16dp below the
banner at the local host boundary.
System and caller-owned navigation are outside the measured SDK letterbox and do not select the
arrangement. A primary action remains at least 16dp above the banner. No-banner Clips retain their
80dp target. Direct/modal Clips retain the 80dp target above the banner, its top-edge track, and 16dp
of non-clickable separation between lower regions. Eligible Embedded ABOVE_ACTION progress also
retains the 80dp target above the primary action and banner.
Hosts that are too short to fit the complete separated stack retain legacy control positions without the new clearance guarantee; the SDK does not hide an eligible banner. 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, the configured synchronous or asynchronous request-builder callback is applied to bottom banner requests as well as native ad requests.
Each request class includes an ItemInfo object that contains:
categories - List of categories associated with the item for ad targeting
contentId - Identifier of the current Story or Clip, available to dynamic ad callbacks. It is intentionally omitted from ItemInfo.toString() so incidental request logging does not expose host content identifiers.
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.
For standard ad-as-Clip placements, the active non-skippable period also blocks switching to Following, moving to the next or previous Clip, and opening category or followable-category navigation. Standard skippable ads retain the existing navigation behavior.
{"slug": "ads", "page_title": "Ads", "page_url": "Ads/", "canonical_url": "/android/Ads/", "markdown": "# Ads\n\n## Table of Contents\n\n<!-- no toc -->\n\n- [Introduction](#introduction)\n + [Choosing an Ad Module](#choosing-an-ad-module)\n + [Installing a Google Ads Product](#installing-a-google-ads-product)\n + [Migrating Between Google Ads Products](#migrating-between-google-ads-products)\n + [Storyteller First Party Ads](#storyteller-first-party-ads)\n + [Clips Opening Ads](#clips-opening-ads)\n + [Per-presentation Clips Ad Controls](#per-presentation-clips-ad-controls)\n + [Storyteller IMA SDK](#storyteller-ima-sdk)\n - [IMA GAM Setup](#ima-gam-setup)\n - [IMA Complete Ad-tag Setup](#ima-complete-ad-tag-setup)\n - [IMA Scope and Failure Behavior](#ima-scope-and-failure-behavior)\n + [Legacy and GAM v24 SDKs](#legacy-and-gam-v24-sdks)\n - [Basic Setup](#basic-setup)\n - [Setup with the dynamic Ad Unit changes](#setup-with-the-dynamic-ad-unit-changes)\n - [Setup with the additional parameters](#setup-with-the-additional-parameters)\n - [Setup with asynchronous request enrichment](#setup-with-asynchronous-request-enrichment)\n + [Storyteller NextGen GAM SDK](#storyteller-nextgen-gam-sdk)\n - [NextGen Basic Setup](#nextgen-basic-setup)\n - [NextGen Swipeable Interstitial Setup](#nextgen-swipeable-interstitial-setup)\n - [NextGen Request-builder Callbacks](#nextgen-request-builder-callbacks)\n + [Storyteller AdMob SDK](#storyteller-admob-sdk)\n - [AdMob Basic Setup](#admob-basic-setup)\n - [AdMob Setup with Banner Fallback](#admob-setup-with-banner-fallback)\n - [AdMob Setup with Bottom Banner Ads](#admob-setup-with-bottom-banner-ads)\n - [AdMob Setup with Key-Value Pairs](#admob-setup-with-key-value-pairs)\n - [AdMob Setup with Banner Priority](#admob-setup-with-banner-priority)\n - [AdMob Test Ad Unit IDs](#admob-test-ad-unit-ids)\n - [Key Differences from GAM](#key-differences-from-gam)\n + [Storyteller VAST SDK](#storyteller-vast-sdk)\n - [VAST Setup](#vast-setup)\n - [GAM VAST Setup](#gam-vast-setup)\n - [GAM VAST Parameter Mapping](#gam-vast-parameter-mapping)\n - [VAST and GAM VAST Parameter Tables](#vast-and-gam-vast-parameter-tables)\n - [VAST URL Formats](#vast-url-formats)\n - [VAST Supported Scope](#vast-supported-scope)\n - [VAST Tracking and Skip Behavior](#vast-tracking-and-skip-behavior)\n - [ITV as the First Validated Scenario](#itv-as-the-first-validated-scenario)\n - [VAST Branded Presentation Metadata](#vast-branded-presentation-metadata)\n - [VAST QA Checklist](#vast-qa-checklist)\n + [Bottom Banner Ads](#bottom-banner-ads)\n + [Ad Source for Custom Modules](#ad-source-for-custom-modules)\n + [Ad Request Information](#ad-request-information)\n - [StorytellerAdRequestInfo](#storytelleradrequestinfo)\n\n## Introduction\n\nThe Storyteller SDK supports ads created in the Storyteller CMS (First Party Ads), Google Ad Manager (GAM), AdMob, VAST tags, true Google IMA pre-roll for Clips, and custom ad modules supplied by an integrator.\n\nWhich source of ads is used can be configured on your behalf by a member of the Storyteller Delivery Team.\n\n### Choosing an Ad Module\n\nChoose exactly one native/banner Google Ads product and use the same exact version as `com.getstoryteller:sdk`:\n\nThe table below describes the `11.7.0` release line. On the separate `12.0.0` line, all Storyteller products use Kotlin 2.1+; the core SDK, Legacy Ads, VAST, and IMA require API 23, while GAM v24 remains API 23 and NextGen remains API 24. See [Choosing the SDK Release Line](GettingStarted.md#choosing-the-sdk-release-line) before upgrading.\n\n| Product | Coordinate | Public facade | Android floor | Google SDK | Kotlin build line | AdMob |\n| ----------- | ----------------------------------------------------- | ------------------------------------------------------------------------------ | ------------- | ------------------------------- | ----------------- | ------------ |\n| Legacy | `com.getstoryteller:ads:<storyteller-version>` | `com.storyteller.modules.ads.StorytellerGamModule` or `StorytellerAdMobModule` | API 21 | Google Mobile Ads 23.4.0 | 1.9 | Supported |\n| GAM v24 | `com.getstoryteller:ads24:<storyteller-version>` | `com.storyteller.modules.ads.StorytellerGamModule` or `StorytellerAdMobModule` | API 23 | Google Mobile Ads 24.6.0 | 2.1 | Supported |\n| NextGen GAM | `com.getstoryteller:adsnextgen:<storyteller-version>` | `com.storyteller.modules.adsnextgen.StorytellerGamModule` | API 24 | Google Mobile Ads NextGen 1.4.0 | 1.9 or newer | Not included |\n\n| Feature | Legacy GAM | GAM v24 | NextGen GAM | AdMob (`ads` or `ads24`) |\n| ------------------------------ | --------------------------------------------------- | --------------------------------------------------- | -------------------------------------------------- | -------------------------------- |\n| Custom Native Templates | Supported | Supported | Supported | Not supported |\n| Key-Value Pair Targeting | Supported | Supported | Supported | Supported through network extras |\n| Publisher Provided ID | Supported | Supported | Not supported | Not applicable |\n| Host request-builder callbacks | Sync or async `AdManagerAdRequest.Builder` callback | Sync or async `AdManagerAdRequest.Builder` callback | Separate sync or async native and banner callbacks | Not supported |\n| Bottom Banner Ads | Supported | Supported | Supported | Supported |\n| Banner Priority Mode | Not supported | Not supported | Not supported | Supported |\n| Swipeable Interstitial Clips | Not supported | Not supported | Supported with GAM activation | Not supported |\n\n!!! warning \"Select exactly one Google Ads product\"\n\n 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`.\n\n`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. The registered module order remains the default fallback order. A Clips presentation can override the eligible standard providers and their order with [`betweenClipsAdProviderOrder`](#per-presentation-clips-ad-controls).\n\n`StorytellerImaModule` is also independent of the native/banner product capability. It lives in `com.getstoryteller:ads-ima` and can be registered alongside Legacy `ads`, GAM v24 `ads24`, or `adsnextgen`. The IMA artifact uses the Media3 IMA integration already supplied by the core SDK, so host apps do not add a separate IMA dependency.\n\n### Installing a Google Ads Product\n\nEnsure Maven Central is available in `settings.gradle`, then give the core SDK and the selected Ads product one shared version:\n\n```groovy\ndependencyResolutionManagement {\n repositories {\n google()\n mavenCentral()\n }\n}\n```\n\n```toml\n[versions]\nstoryteller = \"<storyteller-version>\"\n\n[libraries]\nstoryteller-sdk = { module = \"com.getstoryteller:sdk\", version.ref = \"storyteller\" }\n# Keep exactly one of these Google Ads declarations:\nstoryteller-ads = { module = \"com.getstoryteller:ads\", version.ref = \"storyteller\" }\nstoryteller-ads24 = { module = \"com.getstoryteller:ads24\", version.ref = \"storyteller\" }\nstoryteller-adsnextgen = { module = \"com.getstoryteller:adsnextgen\", version.ref = \"storyteller\" }\n# Optional Clips-only IMA pre-roll, compatible with any one selected product:\nstoryteller-ads-ima = { module = \"com.getstoryteller:ads-ima\", version.ref = \"storyteller\" }\n```\n\n```kotlin\ndependencies {\n implementation(libs.storyteller.sdk)\n implementation(libs.storyteller.ads24) // Or ads / adsnextgen, never more than one.\n implementation(libs.storyteller.ads.ima) // Optional true Clips pre-roll.\n}\n```\n\nAll 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.\n\n### Migrating Between Google Ads Products\n\n| Migration | Source changes | Required checks |\n| ---------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| 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 |\n| Legacy AdMob to GAM v24 | Normally dependency-only because the AdMob 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 AdMob placement |\n| 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 |\n| 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 |\n\nAfter 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.\n\n### Storyteller First Party Ads\n\nIf 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\nentirely within the Storyteller SDK.\n\n### Clips Opening Ads\n\nA standard native opening ad-as-Clip is eligible only when the effective Clips `initialIndex` is `0`. The effective value comes from a valid per-presentation override when supplied, otherwise from remote Clips cadence. A missing or `null` effective initial index never creates an opening ad; it uses frequency-only later cadence, with slots after `frequency`, `2 * frequency`, and subsequent content Clips. A positive effective initial index keeps its configured later between-Clip cadence without inserting an opening ad. Embedded Clips continue to suppress the standard zero-index opening placement while retaining later cadence.\n\nFor 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.\n\nLater between-clips ad requests continue using the normal loading behavior.\n\nWhen a correctly configured `StorytellerImaModule` is registered and `preRollEnabled` is `true`, the SDK instead attempts one true player-level IMA pre-roll before the initially opened Clip, regardless of its index. IMA suppresses only the standard zero-index opening ad-as-Clip. Later native ad cadence and bottom banners are unchanged.\n\n### Per-presentation Clips Ad Controls\n\nUse `StorytellerClipsAdConfiguration` to configure one Clips presentation without changing the process-wide modules registered in `Storyteller.modules`:\n\n```kotlin\nval vastFirst = Storyteller.StorytellerClipsAdConfiguration(\n bottomBannerEnabled = true,\n preRollEnabled = userCanReceivePreRoll,\n betweenClipsAdProviderOrder = listOf(\n Storyteller.StorytellerAdProvider.VAST,\n Storyteller.StorytellerAdProvider.GAM,\n ),\n frequency = 4,\n initialIndex = 1,\n)\n\nval gamFirst = Storyteller.StorytellerClipsAdConfiguration(\n bottomBannerEnabled = true,\n preRollEnabled = userCanReceivePreRoll,\n betweenClipsAdProviderOrder = listOf(\n Storyteller.StorytellerAdProvider.GAM,\n Storyteller.StorytellerAdProvider.VAST,\n ),\n)\n\nval swipeableFirst = Storyteller.StorytellerClipsAdConfiguration(\n bottomBannerEnabled = true,\n preRollEnabled = true,\n betweenClipsAdProviderOrder = listOf(\n Storyteller.StorytellerAdProvider.SWIPEABLE_INTERSTITIAL,\n Storyteller.StorytellerAdProvider.GAM,\n ),\n)\n```\n\nRegister every provider that a presentation may select. The per-presentation order overrides the standard provider order for that request; it does not reorder or mutate the registered module list:\n\n```kotlin\nStoryteller.modules = listOf(\n gamVastModule, // StorytellerAdProvider.VAST\n gamModule, // StorytellerAdProvider.GAM\n imaModule, // Opening pre-roll; configured separately below.\n)\n```\n\nThe configuration has these semantics:\n\n- `bottomBannerEnabled` defaults to `true` and controls only the bottom-banner placement. Embedded Clips still treat an omitted `adConfiguration` as bottom-banner disabled.\n- `preRollEnabled` defaults to `true`. Set it to `false` to suppress both an IMA opening pre-roll and a standard zero-index opening ad-as-Clip for this presentation. It does not disable later remotely scheduled between-Clip ads or bottom banners. The value is fixed for the active opening session; changing it later affects the next presentation.\n- `betweenClipsAdProviderOrder = null` preserves existing behavior: all registered standard/custom modules are attempted in their process-wide order.\n- A populated `betweenClipsAdProviderOrder` is both an order and an allowlist. The SDK attempts only matching recognized providers, in the supplied order. `VAST` selects `StorytellerVastModule` or `StorytellerGamVastModule`; `GAM` selects the registered Legacy, GAM v24, or NextGen GAM module; `ADMOB` selects the registered AdMob module; `SWIPEABLE_INTERSTITIAL` selects the independently configured NextGen GAM swipeable route.\n- `betweenClipsAdProviderOrder = emptyList()` disables standard between-Clip ad requests for this presentation. IMA remains independently controlled by `preRollEnabled`, and bottom banners remain independently controlled by `bottomBannerEnabled`.\n- Duplicate provider values use their first occurrence. An unavailable provider is skipped safely. If multiple modules advertise the same provider, their relative registered order is retained. Modules with no recognized provider capability are excluded by an explicit allowlist but remain available when the value is `null`.\n- `frequency = null` inherits the remote between-Clip frequency. A positive value overrides it for this presentation.\n- `initialIndex = null` inherits the remote initial index. Zero is the special opening-ad value. Positive values are one-based content positions, so `2` schedules the first eligible slot after the second Clip.\n- The two cadence fields resolve independently. An invalid `frequency` (`0` or negative) or `initialIndex` (negative) logs a value-redacted warning and inherits only that remote field.\n- Cadence is fixed when the player opens. Changing a configuration object later affects only a new presentation and never mutates process-wide Settings.\n\nThe remote Clips strategy and ad source remain authoritative. Cadence overrides apply only when remote settings select the eligible between-Clips strategy; they cannot enable ads for an ineligible strategy or source. Consent, registered modules, provider availability, request timeout, no-fill, and player eligibility continue to apply.\n\nProvider attempts are sequential. For example, VAST-first waits for the VAST attempt to fill, fail, or time out before GAM starts. Swipeable and standard GAM attempts also remain independently orderable even though both use the same registered NextGen module. Every fallback attempt keeps the same slot and `adRequestId`. Choose the preferred route first only when its possible load delay is acceptable.\n\nUse named arguments in new integrations. For binary and source compatibility, the existing positional `StorytellerClipsAdConfiguration(false)` form still means `bottomBannerEnabled = false`; it does not disable pre-roll.\n\n### Storyteller IMA SDK\n\nAdd the version-aligned IMA artifact alongside the core SDK and exactly one Legacy, GAM v24, or NextGen Ads product:\n\n```kotlin\ndependencies {\n implementation(\"com.getstoryteller:sdk:<storyteller-version>\")\n implementation(\"com.getstoryteller:ads24:<storyteller-version>\") // Or ads / adsnextgen.\n implementation(\"com.getstoryteller:ads-ima:<storyteller-version>\")\n}\n```\n\nDo not add `androidx.media3:media3-exoplayer-ima` or Google IMA separately. The core Storyteller SDK already provides the compatible Media3 IMA runtime. `ads-ima` supports API 21 and the same Kotlin 1.9 build line as the core SDK.\n\n#### IMA GAM Setup\n\nUse `initWithGam` when your app supplies the GAM ad-unit inputs and Storyteller should construct the video ad tag:\n\n```kotlin\nimport com.storyteller.Storyteller\nimport com.storyteller.modules.ads.StorytellerGamModule\nimport com.storyteller.modules.ima.StorytellerImaModule\n\nval standardAdsModule = StorytellerGamModule.getInstance(applicationContext).apply {\n init(adUnit = { \"/network/storyteller/native\" })\n}\n\nval imaModule = StorytellerImaModule.getInstance(applicationContext).apply {\n initWithGam(\n adUnit = { \"/network/storyteller/video\" },\n descriptionUrl = { \"https://example.com/clips\" },\n contentUrl = { request -> \"https://example.com/clips/${request.itemInfo.contentId}\" },\n customParams = { mapOf(\"placement\" to \"storyteller_clips\") },\n tagParameters = { emptyMap() },\n )\n}\n\nStoryteller.modules = listOf(standardAdsModule, imaModule)\n```\n\nThe callbacks receive `StorytellerAdRequestInfo.ClipsAdRequestInfo` for the opening opportunity. `customParams` are encoded into GAM `cust_params`. The SDK also adds the same [VAST bid defaults](#vast-and-gam-vast-parameter-tables) used by generic VAST and GAM VAST requests, including `adtype=13`, `plcmt=3`, physical-pixel `vw`/`vh`, and mute-aware `vplay`. `tagParameters` are merged last as top-level parameters and may intentionally override any SDK-generated value. These defaults apply only when Storyteller constructs the tag through `initWithGam`; complete tags supplied through `initWithAdTagUrl` are used unchanged.\n\n#### IMA Complete Ad-tag Setup\n\nUse `initWithAdTagUrl` when your integration already constructs the complete HTTPS IMA/VAST tag:\n\n```kotlin\nval imaModule = StorytellerImaModule.getInstance(applicationContext).apply {\n initWithAdTagUrl(\n adTagUrl = { request ->\n buildCompleteImaTag(request.itemInfo.contentId)\n },\n )\n}\n\nStoryteller.modules = listOf(standardAdsModule, imaModule)\n```\n\nThe static `String` overload can be used when one complete tag applies to every opening. Generated tags must be valid HTTPS URLs no longer than 8,192 characters. Optional positive `vastLoadTimeoutMs` and `mediaLoadTimeoutMs` values are forwarded to Media3 IMA. IMA's native phase timeouts remain authoritative. The SDK arms a 60-second fail-open safety watchdog as soon as it claims the opening opportunity while waiting for the transient player context. Player playback gates such as first-use instructions do not defer context creation; the IMA source is prepared while playback remains paused. Once the IMA request/session is created, that timer is replaced by a watchdog that waits at least 60 seconds, or the configured VAST plus media timeout budgets plus 2 seconds when that total is longer.\n\n#### IMA Scope and Failure Behavior\n\n- IMA runs once per Clips opening session, before whichever Clip was initially opened through a tile, `openCollection`, deep link, or Embedded Clips.\n- A single-Clip action presentation is a supported tile/programmatic opening and receives the same one opening opportunity.\n- IMA is eligible only for an initial Default or For You root Clips opening. Direct Following, Clips search, category-feed, initial-category, and category-navigation routes do not request or play an IMA pre-roll.\n- It is supported on Android phones and tablets. Stories and Android TV are not supported.\n- It does not provide IMA interstitials, mid-rolls, repeated cadence, or banners.\n- Invalid configuration, no-fill, IMA or player-side ad playback errors, or timeout hand content to the initially opened Clip once. The SDK does not fall back to a standard zero-index opening ad for that session.\n- If a shared feed/cache update removes or reorders the Clip bound to an opening before content handoff, the SDK releases the IMA session and fails open instead of leaving playback or navigation blocked.\n- Rotation, foreground/background transitions, and ad click-through return do not replay the opening ad. Closing the player cancels the session.\n- A paused Embedded Clips opening prepares and retains its IMA request/session as soon as the route is active, while ExoPlayer still honors the host playback pause. This prevents another Clip from bypassing the opening pre-roll and ensures the fail-open watchdog is bounded even when host playback remains disabled.\n- From opening claim through IMA handoff, content navigation and interactions remain blocked. A skippable IMA ad unlocks navigation after skip hands playback to content; a non-skippable IMA ad unlocks it after completion hands playback to content.\n- Storyteller emits its normal ad analytics only for lifecycle events actually reported by IMA; tag URLs and raw IMA error messages are not included. Pod dismissal analytics bind to the currently started creative, mid-ad errors close that creative with `Finished Ad`, and IMA playback time is excluded from Clip viewing duration.\n\n`StorytellerClipsAdConfiguration.preRollEnabled` is the per-presentation eligibility control. A correctly configured IMA module is still required to serve IMA, but the SDK does not request or claim IMA when this value is `false`. The same value also suppresses the standard zero-index opening ad-as-Clip. It does not affect later cadence or bottom banners.\n\n### Legacy and GAM v24 SDKs\n\nTo 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.\n\nUse either the legacy `ads` product or the GAM v24 `ads24` product described above. Their `StorytellerGamModule` facade stays in the same package, so the following initialization works for both products. Both products also expose `StorytellerAdMobModule`; register only one facade.\n\n#### Basic Setup\n\nNow initialize the extension as follows:\n\n```kotlin\nimport com.storyteller.modules.ads.StorytellerGamModule\nimport com.storyteller.domain.ads.entities.StorytellerAdRequestInfo\n\nval storytellerGamModule = StorytellerGamModule.getInstance(applicationContext).apply {\n init(\n adUnit = { storytellerAdRequestInfo: StorytellerAdRequestInfo -> \"/33813572/storyteller\" },\n )\n}\nfun initializeStoryteller() {\n Storyteller.modules = listOf(storytellerGamModule)\n //initialize code\n}\n```\n\nYou will need to supply the following parameters:\n\n| Parameter Name | Description |\n| ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `{ 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. |\n| `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. |\n| `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. |\n| `configureAdRequestBuilder` | Optional synchronous or asynchronous 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. |\n\nThen pass the newly created instance of the extension to the `modules` property on the `Storyteller` instance:\n\n```kotlin\nStoryteller.modules = listOf(storytellerGamModule)\n```\n\n### Showcase examples\n\n- [Compose \u2014 GAM module init (`StorytellerServiceImpl`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/StorytellerServiceImpl.kt#L61)\n- [Compose \u2014 ad unit + template IDs (`StorytellerGoogleAdInfo`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/ads/entity/StorytellerGoogleAdInfo.kt#L13)\n- [XML \u2014 GAM module init (`StorytellerServiceImpl`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/StorytellerServiceImpl.kt#L64)\n- [XML \u2014 ad unit + template IDs (`StorytellerGoogleAdInfo`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/xml/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/ads/entity/StorytellerGoogleAdInfo.kt#L13)\n\n#### Setup with the dynamic Ad Unit changes\n\nExample for dynamic Ad Unit changes when we want to use different adUnit for Stories and Clips:\n\n```kotlin\nimport com.storyteller.modules.ads.StorytellerGamModule\nimport com.storyteller.domain.ads.entities.StorytellerAdRequestInfo\n\nval storytellerGamModule = StorytellerGamModule.getInstance(applicationContext).apply {\n init(\n adUnit = { storytellerAdRequestInfo: StorytellerAdRequestInfo ->\n when (storytellerAdRequestInfo) {\n is StorytellerAdRequestInfo.StoriesAdRequestInfo -> \"/33813572/storyteller/stories\"\n else -> \"/33813572/storyteller/clips\"\n }\n },\n )\n}\n\nfun initializeStoryteller() {\n Storyteller.modules = listOf(storytellerGamModule)\n //initialize code\n}\n```\n\n#### Setup with the additional parameters\n\nYou 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:\n\n```kotlin\nimport com.google.android.gms.ads.admanager.AdManagerAdRequest\nimport com.storyteller.modules.ads.StorytellerGamModule\nimport com.storyteller.domain.ads.entities.StorytellerAdRequestInfo\nimport com.storyteller.domain.ads.entities.StorytellerCustomNativeTemplateIds\n\nval storytellerGamModule = StorytellerGamModule.getInstance(applicationContext).apply {\n init(\n adUnit = { storytellerAdRequestInfo: StorytellerAdRequestInfo ->\n when (storytellerAdRequestInfo) {\n is StorytellerAdRequestInfo.StoriesAdRequestInfo -> \"/33813572/storyteller/stories\"\n else -> \"/33813572/storyteller/clips\"\n }\n },\n templateIds = StorytellerCustomNativeTemplateIds(\"12102683\", \"12269089\"),\n keyValuePairs = {\n mapOf(\n \"ddid\" to developerDeviceId,\n \"isLoggedIn\" to isLoggedIn.toString(),\n )\n },\n publisherProvidedId = { ciamId ?: developerDeviceId },\n configureAdRequestBuilder = { adRequestInfo: StorytellerAdRequestInfo, builder: AdManagerAdRequest.Builder ->\n bidderRequestBuilder(adRequestInfo)?.applyTo(builder)\n },\n )\n}\n\nfun initializeStoryteller() {\n Storyteller.modules = listOf(storytellerGamModule)\n //initialize code\n}\n```\n\n| Parameter Name | Description |\n| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `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. |\n| `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. |\n| `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. |\n| `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. The two-argument callback mutates the builder synchronously; the three-argument overload waits for explicit completion 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. |\n\nBy default, Storyteller always includes the following GAM KVPs:\n\n| Request Type | Default KVPs |\n| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| 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). |\n| 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). |\n\n`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`.\n\n`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.\n\n#### Setup with asynchronous request enrichment\n\nUse the three-argument `configureAdRequestBuilder` overload when a bidder SDK completes later. The following is conceptual\u2014replace `bidder` and `BidderResult` with your bidder SDK's API:\n\n```kotlin\nstorytellerGamModule.init(\n adUnit = { \"/33813572/storyteller\" },\n configureAdRequestBuilder = { requestInfo, builder, completion ->\n bidder.loadDemand(requestInfo, builder) { result ->\n when (result) {\n is BidderResult.Success -> completion.onSuccess(result.builder)\n BidderResult.NoFill -> completion.onNoFill()\n is BidderResult.Failure -> completion.onFailure()\n }\n }\n },\n)\n```\n\nStoryteller waits to build and load that request until the callback completes. Call exactly one terminal method:\n\n| Completion | Behavior |\n| ---------- | -------- |\n| `onSuccess(builder)` | Continues with the supplied prepared or replacement builder. Storyteller reapplies its reserved targeting, client KVPs, and PPID before load while preserving other host `AdMobAdapter` network extras. Storyteller-owned keys take precedence on collision. |\n| `onNoFill()` | Continues with a clean Storyteller request without bidder enrichment, discarding any partial mutations made before no-fill. |\n| `onFailure()` | Continues with a clean Storyteller request without bidder enrichment, discarding any partial mutations made before failure. |\n\nThe first terminal call wins; duplicate or late completions are ignored. If the callback throws before completing, Storyteller treats it as `onFailure()`. If the Storyteller ad context is cancelled while enrichment is pending, the request is not loaded and later completion is ignored.\n\nThe host owns bidder timeout policy. Every configured asynchronous callback must eventually call one terminal method unless the ad context is cancelled. The existing two-argument callback remains synchronous and source/binary compatible.\n\nWhen 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.\n\n### Storyteller NextGen GAM SDK\n\nThe `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.\n\nNextGen 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.\n\n#### NextGen Basic Setup\n\nPass your Google Ads application ID to `getInstance`, configure the module, and register it before `Storyteller.initialize(...)`:\n\n```kotlin\nimport com.storyteller.domain.ads.entities.StorytellerAdRequestInfo\nimport com.storyteller.domain.ads.entities.StorytellerCustomNativeTemplateIds\nimport com.storyteller.modules.adsnextgen.StorytellerGamModule\n\nval storytellerGamModule = StorytellerGamModule.getInstance(\n applicationContext = applicationContext,\n adAppId = \"ca-app-pub-0000000000000000~0000000000\",\n).apply {\n init(\n adUnit = { requestInfo: StorytellerAdRequestInfo ->\n when (requestInfo) {\n is StorytellerAdRequestInfo.StoriesAdRequestInfo -> \"/33813572/storyteller/stories\"\n else -> \"/33813572/storyteller/clips\"\n }\n },\n templateIds = StorytellerCustomNativeTemplateIds(\"12102683\", \"12269089\"),\n keyValuePairs = { mapOf(\"appmode\" to \"prod\") },\n bottomBannerAdUnit = { \"/33813572/storyteller/clips_banner\" },\n )\n}\n\nStoryteller.modules = listOf(storytellerGamModule)\n```\n\nThe 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.\n\n#### NextGen Swipeable Interstitial Setup\n\nNextGen GAM can render Google's swipeable-interstitial creative as an exact native view between Clips. Configure its ad unit independently of `init(...)`, then include `SWIPEABLE_INTERSTITIAL` in the presentation's provider order:\n\n```kotlin\nstorytellerGamModule.setClipsSwipeableInterstitialAdUnit { requestInfo ->\n // Resolve the swipeable unit for this Clips request.\n \"/1234567/clips/swipeable-interstitial\"\n}\n\nval adConfiguration = Storyteller.StorytellerClipsAdConfiguration(\n betweenClipsAdProviderOrder = listOf(\n Storyteller.StorytellerAdProvider.SWIPEABLE_INTERSTITIAL,\n Storyteller.StorytellerAdProvider.GAM,\n ),\n)\n```\n\nThe setter composes with every synchronous and asynchronous `init(...)` overload. Pass `null` to disable the route. Returning `null` or blank for one request makes that attempt fail immediately and continue to the next configured provider. Swipeable requests receive the same consent-gated Storyteller and client key-value targeting as standard NextGen GAM requests.\n\n`SWIPEABLE_INTERSTITIAL` applies only to later between-Clip slots in direct and Embedded Clips. It is never used for Stories, the standard zero-index opening ad, IMA pre-roll, or bottom banners. `GAM` continues to select the standard NextGen native/banner route, so either route can precede the other without registering a second module. A Swipeable-only provider list is also supported.\n\nStoryteller passes the exact view returned by Google's `SwipeableInterstitialAd.getView(Activity)` into the Clips renderer and destroys the Google ad when the SDK ad view is disposed. Google owns taps and horizontal creative interactions; vertical drags belong to Clips paging and do not scroll the ad document. Storyteller does not request Google's optional screen-hold timer or custom click gestures, so the pinned Google defaults remain zero seconds and disabled. Storyteller treats the page as immediately skippable interactive content rather than video playback and does not add a second countdown. A request that does not complete within 10 seconds fails through to the next provider, and any later Google result is destroyed.\n\nOn phones and tablets, Storyteller requests a canvas based on the current window's safe content area in dp, excluding system bars and display cutouts. The complete ad surface is proportionally fitted into the actual Clips page, including when an Embedded host reserves space for bottom navigation. The `320x480` value below describes Google's reference creative rather than a fixed SDK request size.\n\nWhen a tablet changes orientation, Storyteller removes the visible Swipeable ad and shows the next available Clip without an animated scroll or advertising loader. If there is no following Clip, it returns to the previous available Clip. The removed ad placement is not requested again during that feed session. If a regular Clip is visible, it stays selected. Future Swipeable placements are reloaded in the background using the settled dimensions and are skipped if the viewer passes them before an ad is ready. Other changes to request-canvas dimensions, density, or Activity identity still replace incompatible creatives after layout settles.\n\nBefore a Swipeable ad enters the feed, Storyteller measures its content and prepares its native viewport while hidden. The complete measured surface is uniformly scaled, top aligned, and horizontally centered within the safe player area. This behavior applies to both debug and release apps without additional configuration. There are no fixed 10% margins, and the SDK does not rewrite creative HTML. Preparation preserves Google View identities and adjusts native viewport dimensions before display; a creative that cannot be prepared within the bounded readiness and resize limits is discarded and the configured provider fallback continues. Resizing the available area without changing the safe-window request canvas updates only the outer fit. A response exceeding its requested canvas is rejected and follows the configured provider fallback.\n\nHost apps do not need to supply sizes or change their integration. Ordinary Clips and other ad formats retain their existing layout policies. System and explicit host insets are respected. Only on landscape tablets with For You / Following tabs enabled, Swipeable reserves 48 dp below them in addition to the host top inset and fits within the remaining height. Phones and portrait tablets have no additional header gap.\n\n!!! warning \"GAM activation and creative requirements\"\n\n Swipeable interstitial availability and trafficking are controlled by Google and your GAM account. Coordinate activation with your Google account team and Storyteller Delivery before production use. Traffic a compatible swipeable-interstitial creative to the configured unit; Google's reference format uses a `320x480` creative. Keep the standard GAM route configured as fallback until production delivery is verified.\n\nAnalytics retain `adSource = \"gam\"` because both routes use GAM. Swipeable request, failure, loaded, paid, and rendered lifecycle events use `adFormat = \"swipeableInterstitial\"`; fallback attempts share the same `adRequestId`, and the winning route owns later lifecycle attribution.\n\n#### NextGen Request-builder Callbacks\n\nNextGen exposes separate callbacks because Google uses distinct builders for native and banner requests:\n\n```kotlin\nimport com.google.android.libraries.ads.mobile.sdk.banner.BannerAdRequest\nimport com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdRequest\n\nstorytellerGamModule.init(\n adUnit = { \"/33813572/storyteller\" },\n bottomBannerAdUnit = { \"/33813572/storyteller/clips_banner\" },\n configureNativeAdRequestBuilder = { requestInfo, builder: NativeAdRequest.Builder ->\n bidderTargeting(requestInfo).forEach { (key, value) ->\n builder.putCustomTargeting(key, value)\n }\n },\n configureBannerAdRequestBuilder = { requestInfo, builder: BannerAdRequest.Builder ->\n bidderTargeting(requestInfo).forEach { (key, value) ->\n builder.putCustomTargeting(key, value)\n }\n },\n)\n```\n\nStoryteller 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.\n\nFor asynchronous bidder work, use the equivalent three-argument callbacks:\n\n```kotlin\nstorytellerGamModule.init(\n adUnit = { \"/33813572/storyteller\" },\n bottomBannerAdUnit = { \"/33813572/storyteller/clips_banner\" },\n configureNativeAdRequestBuilder = { requestInfo, builder, completion ->\n bidder.loadNativeDemand(requestInfo, builder) { result ->\n when (result) {\n is BidderResult.Success -> completion.onSuccess(result.builder)\n BidderResult.NoFill -> completion.onNoFill()\n is BidderResult.Failure -> completion.onFailure()\n }\n }\n },\n configureBannerAdRequestBuilder = { requestInfo, builder, completion ->\n bidder.loadBannerDemand(requestInfo, builder) { result ->\n when (result) {\n is BidderResult.Success -> completion.onSuccess(result.builder)\n BidderResult.NoFill -> completion.onNoFill()\n is BidderResult.Failure -> completion.onFailure()\n }\n }\n },\n)\n```\n\nThe completion, cancellation, exception, duplicate-call, and host-owned timeout semantics are the same as Legacy/GAM v24. When `onSuccess` supplies a replacement NextGen builder, construct it for the same ad unit and requested ad types or ad sizes as the prepared builder. Storyteller reapplies the mutable request setup it owns, including reserved targeting, client KVPs, ad size/options, and custom-template IDs.\n\n### Storyteller AdMob SDK\n\nThe 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.\n\n**Important**: AdMob is available from either the legacy `ads` product or the GAM v24 `ads24` product in 11.5.1 and 11.6.1 or newer. Version 11.6.0 omitted `StorytellerAdMobModule` from `ads24`; upgrade to 11.6.1 or newer instead of adapting application code. Select exactly one Google Ads product and register only one GAM or AdMob facade.\n\nTo use this extension, first install it using Gradle. Ensure Maven Central is available in your `settings.gradle`:\n\n```groovy\nmavenCentral()\n```\n\nThen add exactly one of the following references to your version catalog file:\n\n```toml\nstoryteller-ads = { module = \"com.getstoryteller:ads\", version.ref = \"storyteller\" }\nstoryteller-ads24 = { module = \"com.getstoryteller:ads24\", version.ref = \"storyteller\" }\n```\n\nAnd finally reference this in your `build.gradle`:\n\n```groovy\nimplementation(libs.storyteller.ads24) // Or storyteller.ads, never both.\n```\n\n#### AdMob Basic Setup\n\nInitialize the AdMob module as follows:\n\n```kotlin\nimport com.storyteller.modules.ads.StorytellerAdMobModule\nimport com.storyteller.domain.ads.entities.StorytellerAdRequestInfo\n\nval adMobModule = StorytellerAdMobModule.getInstance(applicationContext).apply {\n init(\n nativeAdUnit = { adRequestInfo: StorytellerAdRequestInfo -> \"ca-app-pub-xxx/yyy\" },\n )\n}\n\nfun initializeStoryteller() {\n Storyteller.modules = listOf(adMobModule)\n // initialize code\n}\n```\n\nYou will need to supply the following parameters:\n\n| Parameter Name | Description |\n| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `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. |\n| `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). |\n| `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. |\n| `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. |\n| `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. |\n\nThen pass the newly created instance of the extension to the `modules` property on the `Storyteller` instance:\n\n```kotlin\nStoryteller.modules = listOf(adMobModule)\n```\n\nExample for dynamic Ad Unit changes when you want to use different ad units for Stories and Clips:\n\n```kotlin\nimport com.storyteller.modules.ads.StorytellerAdMobModule\nimport com.storyteller.domain.ads.entities.StorytellerAdRequestInfo\n\nval adMobModule = StorytellerAdMobModule.getInstance(applicationContext).apply {\n init(\n nativeAdUnit = { adRequestInfo: StorytellerAdRequestInfo ->\n when (adRequestInfo) {\n is StorytellerAdRequestInfo.StoriesAdRequestInfo -> \"ca-app-pub-xxx/stories\"\n else -> \"ca-app-pub-xxx/clips\"\n }\n },\n )\n}\n\nfun initializeStoryteller() {\n Storyteller.modules = listOf(adMobModule)\n // initialize code\n}\n```\n\n#### AdMob Setup with Banner Fallback\n\nThe 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:\n\n```kotlin\nimport com.storyteller.modules.ads.StorytellerAdMobModule\nimport com.storyteller.domain.ads.entities.StorytellerAdRequestInfo\n\nval adMobModule = StorytellerAdMobModule.getInstance(applicationContext).apply {\n init(\n nativeAdUnit = { adRequestInfo: StorytellerAdRequestInfo -> \"ca-app-pub-xxx/native\" },\n bannerAdUnit = { adRequestInfo: StorytellerAdRequestInfo -> \"ca-app-pub-xxx/banner_fallback\" },\n )\n}\n\nfun initializeStoryteller() {\n Storyteller.modules = listOf(adMobModule)\n // initialize code\n}\n```\n\nThe fallback flow works as follows:\n\n1. The module first attempts to load a native ad using `nativeAdUnit`\n2. If the native ad fails to load, and `bannerAdUnit` is configured, it attempts to load a banner ad\n3. If both requests fail, the combined error is reported\n\n#### AdMob Setup with Bottom Banner Ads\n\nTo enable bottom banner ads in Clips, provide the `bottomBannerAdUnit` parameter:\n\n```kotlin\nimport com.storyteller.modules.ads.StorytellerAdMobModule\nimport com.storyteller.domain.ads.entities.StorytellerAdRequestInfo\n\nval adMobModule = StorytellerAdMobModule.getInstance(applicationContext).apply {\n init(\n nativeAdUnit = { adRequestInfo: StorytellerAdRequestInfo -> \"ca-app-pub-xxx/native\" },\n bottomBannerAdUnit = { adRequestInfo: StorytellerAdRequestInfo -> \"ca-app-pub-xxx/bottom_banner\" },\n )\n}\n\nfun initializeStoryteller() {\n Storyteller.modules = listOf(adMobModule)\n // initialize code\n}\n```\n\n#### AdMob Setup with Key-Value Pairs\n\nThe AdMob module supports custom key-value pairs for ad targeting. These are passed as network extras to AdMob:\n\n```kotlin\nimport com.storyteller.modules.ads.StorytellerAdMobModule\nimport com.storyteller.domain.ads.entities.StorytellerAdRequestInfo\n\nval adMobModule = StorytellerAdMobModule.getInstance(applicationContext).apply {\n init(\n nativeAdUnit = { adRequestInfo: StorytellerAdRequestInfo -> \"ca-app-pub-xxx/native\" },\n keyValuePairs = { mapOf(\"customKey\" to \"customValue\", \"targeting\" to \"premium\") },\n )\n}\n\nfun initializeStoryteller() {\n Storyteller.modules = listOf(adMobModule)\n // initialize code\n}\n```\n\n**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.\n\n#### AdMob Setup with Banner Priority\n\nBy default, the AdMob module attempts to load native ads first, with banner ads as a fallback. You can reverse this priority using `enableBannerAdPriority`:\n\n```kotlin\nimport com.storyteller.modules.ads.StorytellerAdMobModule\nimport com.storyteller.domain.ads.entities.StorytellerAdRequestInfo\n\nval adMobModule = StorytellerAdMobModule.getInstance(applicationContext).apply {\n init(\n nativeAdUnit = { adRequestInfo: StorytellerAdRequestInfo -> \"ca-app-pub-xxx/native\" },\n bannerAdUnit = { adRequestInfo: StorytellerAdRequestInfo -> \"ca-app-pub-xxx/banner\" },\n enableBannerAdPriority = true, // Try banner first, native as fallback\n )\n}\n\nfun initializeStoryteller() {\n Storyteller.modules = listOf(adMobModule)\n // initialize code\n}\n```\n\nWhen `enableBannerAdPriority` is `true`:\n\n1. The module first attempts to load a banner ad using `bannerAdUnit`\n2. If the banner ad fails to load, it attempts to load a native ad using `nativeAdUnit`\n3. If both requests fail, the combined error is reported\n\n**Note**: `enableBannerAdPriority` only takes effect when `bannerAdUnit` is also configured. If `bannerAdUnit` is not provided, native ads will be loaded regardless of this setting.\n\n#### AdMob Test Ad Unit IDs\n\nFor development and testing, use Google's official test ad unit IDs to avoid generating invalid impressions:\n\n| Ad Type | Test Ad Unit ID |\n| --------------- | ---------------------------------------- |\n| Native Advanced | `ca-app-pub-3940256099942544/2247696110` |\n| Banner | `ca-app-pub-3940256099942544/6300978111` |\n\nExample setup with test ad units:\n\n```kotlin\nval adMobModule = StorytellerAdMobModule.getInstance(applicationContext).apply {\n init(\n nativeAdUnit = { _ -> \"ca-app-pub-3940256099942544/2247696110\" },\n bannerAdUnit = { _ -> \"ca-app-pub-3940256099942544/6300978111\" },\n )\n}\n```\n\n**Note**: Replace these test ad unit IDs with your production ad unit IDs before releasing your app.\n\n#### Key Differences from GAM\n\nWhen choosing between `StorytellerAdMobModule` and `StorytellerGamModule`, consider the following differences:\n\n| Feature | StorytellerGamModule | StorytellerAdMobModule |\n| ---------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------ |\n| **Custom Native Templates** | Supported via `templateIds` parameter | Not supported (AdMob limitation) |\n| **Key-Value Pair Targeting** | Supported via `keyValuePairs` parameter | Supported via `keyValuePairs` (network extras) |\n| **Ad Loading Strategy** | Unified AdLoader supports multiple formats in single request | Native ad first (or banner first with `enableBannerAdPriority`), then fallback |\n| **Banner Priority Mode** | Not supported | Supported via `enableBannerAdPriority` |\n| **Bottom Banner Ads** | Supported | Supported |\n| **Ad Unit Format** | GAM format: `/network/ad_unit` | AdMob format: `ca-app-pub-xxx/yyy` |\n\n**When to use AdMob Module**:\n\n- Your app uses standard AdMob and does not have a Google Ad Manager account\n- You do not need custom native ad templates\n- You want configurable ad loading priority (banner vs native first)\n- You want a simpler integration with automatic banner fallback\n\n**When to use GAM Module**:\n\n- You have a Google Ad Manager account\n- You need custom native ad templates configured with the Storyteller Delivery team\n- You need unified ad loading with multiple formats in a single request\n- You need the default set of Storyteller KVPs automatically included (e.g., `stApiKey`, `stCollection`, `stClipCategories`)\n\n### Storyteller VAST SDK\n\nThe 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.\n\n#### VAST Setup\n\nFor SDK `11.5.1` or later, add the version-aligned `com.getstoryteller:ads-vast` artifact from Maven Central:\n\n```toml\nstoryteller-ads-vast = { module = \"com.getstoryteller:ads-vast\", version.ref = \"storyteller\" }\n```\n\n```groovy\nimplementation(libs.storyteller.ads.vast)\n```\n\nInitialize the module with your VAST endpoint and request parameters:\n\n```kotlin\nimport com.storyteller.modules.vast.StorytellerVastModule\nimport com.storyteller.modules.vast.UrlFormat\n\nval vastModule = StorytellerVastModule.getInstance(applicationContext).apply {\n init(\n baseUrl = \"https://example.com/vast\",\n requestParameters = { adRequestInfo ->\n mapOf(\n \"placementId\" to \"home-preroll\",\n \"adIndex\" to \"0\",\n )\n },\n urlFormat = UrlFormat.QueryString,\n )\n}\n\nfun initializeStoryteller() {\n Storyteller.modules = listOf(vastModule)\n // initialize code\n}\n```\n\nThe `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.\n\nVerbose 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.\n\nFor fallback scenarios, registered module order controls the default priority. For example, `Storyteller.modules = listOf(vastModule, gamModule)` asks VAST first, then GAM if VAST does not fill. A Clips presentation with a non-null `betweenClipsAdProviderOrder` overrides that standard-provider order for its own requests; other presentations and Stories retain the registered order. Follow the module and presentation configuration provided by the Storyteller Delivery team for your tenant.\n\n#### GAM VAST Setup\n\nUse `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`.\n\n```kotlin\nimport com.storyteller.modules.vast.StorytellerGamVastModule\n\nval gamVastModule = StorytellerGamVastModule.getInstance(applicationContext).apply {\n init(\n adUnit = { \"/1234567/vast_video\" },\n descriptionUrl = { \"https://publisher.example/story\" },\n contentUrl = { \"https://publisher.example/content\" },\n customParams = {\n mapOf(\n \"content_category\" to \"sports\",\n )\n },\n tagParameters = { emptyMap() },\n )\n}\n\nfun initializeStoryteller() {\n Storyteller.modules = listOf(gamVastModule)\n // initialize code\n}\n```\n\n`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](#vast-and-gam-vast-parameter-tables).\n\n#### GAM VAST Parameter Mapping\n\n| Callback | GAM parameter | Notes |\n| ---------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `adUnit` | `iu` | Required GAM ad unit path. |\n| `descriptionUrl` | `description_url` | Required page or content description URL. |\n| `contentUrl` | `url` | Optional content URL; blank values are omitted. |\n| `customParams` | `cust_params` | Serialized as nested key-value pairs, for example `content_category=sports`. |\n| `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`. |\n\nSerialization examples:\n\n| Input | Serialized output |\n| ------------------------------------------------------------- | ------------------------------------------------- |\n| `customParams = mapOf(\"content_category\" to \"sports\")` | `cust_params=content_category%3Dsports` |\n| `customParams = mapOf(\"section\" to \"news&sport\")` | `cust_params=section%3Dnews%2526sport` |\n| `tagParameters = mapOf(\"sz\" to \"1080x1920\")` | Overrides the SDK-calculated `sz` value. |\n| `tagParameters = mapOf(\"cust_params\" to \"client_override=1\")` | Overrides the value produced from `customParams`. |\n\n#### VAST and GAM VAST Parameter Tables\n\nVAST 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.\n\n| Parameter | Applies to | Set by | Override path |\n| ------------------ | ------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------- |\n| `iu` | GAM VAST | `adUnit` callback | `tagParameters[\"iu\"]` |\n| `description_url` | GAM VAST | `descriptionUrl` callback | `tagParameters[\"description_url\"]` |\n| `url` | GAM VAST | `contentUrl` callback when non-blank | `tagParameters[\"url\"]` |\n| `cust_params` | GAM VAST | Encoded `customParams` map | `tagParameters[\"cust_params\"]` replaces the encoded map |\n| `content_category` | GAM VAST custom targeting | `customParams[\"content_category\"]` | Supply a different `customParams` value, or override all `cust_params` through `tagParameters[\"cust_params\"]` |\n\nThe 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`.\n\n| Parameter | Value | Source | Type | Meaning | Override path |\n| ------------ | ------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------- | ------------------------------- | ----------------------------- |\n| `output` | `vast` | SDK constant | Static | Request VAST XML response | `tagParameters[\"output\"]` |\n| `env` | `vp` | SDK constant | Static | Video player environment | `tagParameters[\"env\"]` |\n| `gdfp_req` | `1` | SDK constant | Static | GAM ad request marker | `tagParameters[\"gdfp_req\"]` |\n| `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\"]` |\n| `correlator` | Generated numeric value | SDK request counter | Dynamic per request | Request cache-busting | `tagParameters[\"correlator\"]` |\n| `vpa` | `auto` | SDK constant | Static | Video playback automatic | `tagParameters[\"vpa\"]` |\n| `vpmute` | `1` when muted, `0` when unmuted | Current `AdContext.isMuted` | Dynamic per request | Player mute state | `tagParameters[\"vpmute\"]` |\n\nThe SDK sets these bid defaults on generic VAST, GAM VAST, and IMA GAM-generated requests. For IMA GAM-generated tags, use the same keys in `tagParameters` to override them:\n\n| Parameter | SDK value or source | Static/dynamic | Meaning | Override path |\n| --------- | ---------------------------------------------------------- | -------------- | ----------------------- | -------------------------------------------------------------------------- |\n| `adtype` | `13` | Static | Ad type | Generic `requestParameters[\"adtype\"]`; GAM/IMA `tagParameters[\"adtype\"]` |\n| `plcmt` | `3` | Static | Placement type | Generic `requestParameters[\"plcmt\"]`; GAM/IMA `tagParameters[\"plcmt\"]` |\n| `vw` | Current Android window or screen width in physical pixels | Dynamic | Viewport width | Generic `requestParameters[\"vw\"]`; GAM/IMA `tagParameters[\"vw\"]` |\n| `vh` | Current Android window or screen height in physical pixels | Dynamic | Viewport height | Generic `requestParameters[\"vh\"]`; GAM/IMA `tagParameters[\"vh\"]` |\n| `vminl` | `5` | Static | Minimum video duration | Generic `requestParameters[\"vminl\"]`; GAM/IMA `tagParameters[\"vminl\"]` |\n| `vmaxl` | `30` | Static | Maximum video duration | Generic `requestParameters[\"vmaxl\"]`; GAM/IMA `tagParameters[\"vmaxl\"]` |\n| `vfmt` | `1+5` | Static | Accepted video formats | Generic `requestParameters[\"vfmt\"]`; GAM/IMA `tagParameters[\"vfmt\"]` |\n| `vadFmt` | `2+3+8` | Static | Accepted ad formats | Generic `requestParameters[\"vadFmt\"]`; GAM/IMA `tagParameters[\"vadFmt\"]` |\n| `vplay` | `6` for muted autoplay, `5` for sound-on autoplay | Dynamic | Playback mode | Generic `requestParameters[\"vplay\"]`; GAM/IMA `tagParameters[\"vplay\"]` |\n| `vskip` | `1` | Static | Skippable ad signal | Generic `requestParameters[\"vskip\"]`; GAM/IMA `tagParameters[\"vskip\"]` |\n| `vpos` | `0` | Static | Position signal | Generic `requestParameters[\"vpos\"]`; GAM/IMA `tagParameters[\"vpos\"]` |\n| `vcom` | `0` | Static | Companion ad signal | Generic `requestParameters[\"vcom\"]`; GAM/IMA `tagParameters[\"vcom\"]` |\n| `vcont` | `1` | Static | Content type signal | Generic `requestParameters[\"vcont\"]`; GAM/IMA `tagParameters[\"vcont\"]` |\n| `vtype` | `1` | Static | Video type signal | Generic `requestParameters[\"vtype\"]`; GAM/IMA `tagParameters[\"vtype\"]` |\n| `vminbtr` | `600` | Static | Minimum bitrate in Kbps | Generic `requestParameters[\"vminbtr\"]`; GAM/IMA `tagParameters[\"vminbtr\"]` |\n| `vmaxbtr` | `8000` | Static | Maximum bitrate in Kbps | Generic `requestParameters[\"vmaxbtr\"]`; GAM/IMA `tagParameters[\"vmaxbtr\"]` |\n\n#### VAST URL Formats\n\n`UrlFormat` controls how the `requestParameters` map is serialized into the final VAST tag URL:\n\n| Format | Output shape | Use when |\n| ----------------------- | -------------------------------------- | ---------------------------------------------------------- |\n| `UrlFormat.PathSegment` | `https://example.com/vast/k1=v1/k2=v2` | Your VAST server expects key-value pairs as path segments. |\n| `UrlFormat.QueryString` | `https://example.com/vast?k1=v1&k2=v2` | Your VAST server expects standard query parameters. |\n\nPath-segment example:\n\n```kotlin\nval vastModule = StorytellerVastModule.getInstance(applicationContext).apply {\n init(\n baseUrl = \"https://example.com/vast\",\n requestParameters = { adRequestInfo ->\n mapOf(\n \"slot\" to \"video-preroll\",\n \"placement\" to \"home\",\n \"adIndex\" to \"0\",\n )\n },\n urlFormat = UrlFormat.PathSegment,\n )\n}\n```\n\nQuery-string example:\n\n```kotlin\nval vastModule = StorytellerVastModule.getInstance(applicationContext).apply {\n init(\n baseUrl = \"https://example.com/vast\",\n requestParameters = { adRequestInfo ->\n mapOf(\n \"placementId\" to \"home-preroll\",\n \"placement\" to \"home\",\n \"adIndex\" to \"0\",\n )\n },\n urlFormat = UrlFormat.QueryString,\n )\n}\n```\n\nThe 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.\n\n#### VAST Supported Scope\n\nThe first public release supports:\n\n- Inline linear video ads.\n- Compatible VAST 2.0, 3.0, 4.0, 4.1, and 4.2 parsing.\n- Wrapper resolution with depth and loop protection.\n- Media file selection for playable video creatives.\n- Click-through and click tracking.\n- Impression and playback tracking.\n- Skip tracking.\n- Deferred VAST error handling when another module may still fill the same ad slot.\n- Static-resource VAST Icon overlay rendering for Stories and Clips ad video players.\n- Parser-only caption capture for future SDK work.\n\nThe following VAST features are deferred or out of scope for v1:\n\n- Nonlinear ads.\n- Companion ads.\n- OMID or verification rendering.\n- VPAID execution.\n- Server-side ad insertion behavior.\n- VAST Icon `IframeResource` and `HTMLResource` rendering.\n- Ad caption rendering in v1.\n- Bottom banner ads through the VAST module.\n\n#### VAST Tracking and Skip Behavior\n\nVAST 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.\n\n#### VAST Icon Overlay Rendering\n\nVAST `<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.\n\nThe 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.\n\nThe 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.\n\nWhen 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.\n\n`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.\n\nVAST `<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.\n\n#### ITV as the First Validated Scenario\n\nITV 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.\n\nFor 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.\n\n#### VAST Branded Presentation Metadata\n\nVAST ads can provide Storyteller presentation metadata through `Extension type=\"storyteller:ad-ui\"`. The SDK reads `CtaText` and `AdvertiserName` from the `StorytellerAdUi` child element:\n\n```xml\n<Extension type=\"storyteller:ad-ui\">\n <StorytellerAdUi>\n <CtaText>Shop now<\/CtaText>\n <AdvertiserName>Example Brand<\/AdvertiserName>\n <\/StorytellerAdUi>\n<\/Extension>\n```\n\nThe SDK resolves the final presentation values with this priority:\n\n| Field | Resolution priority |\n| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |\n| CTA text | VAST extension `CtaText` -> backend default `ads.presentationDefaults.vast.ctaText` -> VAST `<AdTitle>` -> VAST `<Advertiser>` -> `Learn more` |\n| Advertiser display name | VAST extension `AdvertiserName` -> backend default `ads.presentationDefaults.vast.advertiserName` -> VAST `<Advertiser>` -> VAST `<AdTitle>` |\n\nCTA 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.\n\n#### VAST QA Checklist\n\nBefore enabling a tenant for VAST, validate:\n\n- The app uses SDK `11.5.1` or later and resolves `com.getstoryteller:ads-vast:<version>` from Maven Central at the same version as `com.getstoryteller:sdk`.\n- `StorytellerVastModule` is initialized before `Storyteller.initialize(...)`.\n- `StorytellerGamVastModule` is initialized before `Storyteller.initialize(...)` when the tenant uses a GAM VAST setup.\n- The configured URL format matches the VAST server contract.\n- GAM VAST `customParams` and `tagParameters` serialize into the expected GAM tag URL, and `tagParameters` overrides are intentional.\n- If the tenant uses path-segment URLs, such as an ITV-style setup, that configuration works end to end.\n- A generic query-string VAST configuration works against a query-string VAST test server when the tenant uses query-string URL format.\n- Inline video ad playback reaches impression, quartile, complete, click, and skip tracking paths.\n- Multiple tracking URLs for the same event are preserved.\n- Fallback behavior is correct when VAST returns no ad, malformed XML, no compatible media, or wrapper failures.\n- VAST `skipoffset` behavior matches the tenant's non-skippable ad configuration.\n\n### Bottom Banner Ads\n\nThe Clips Player supports bottom banner ads rendered as standard banner views in the lower player\nregion. With a banner and effective `BOTTOM` progress placement, Embedded Clips use a compact 8dp\nprogress region matching the track's maximum visual height, without a larger scrub-target reservation.\n\nWhen width-constrained video creates enough measured bottom letterbox for the banner and its 16dp\nlower band, the visible track uses the region's top edge at a fixed 16dp before the banner. The banner\nremains 16dp from the local host boundary; extra letterbox height does not increase the track-to-banner\ngap. Without qualifying letterbox space, preserve the existing banner-before-progress arrangement:\nthe banner remains 8dp above the compact progress region, and its bottom-edge track is 16dp below the\nbanner at the local host boundary.\n\nSystem and caller-owned navigation are outside the measured SDK letterbox and do not select the\narrangement. A primary action remains at least 16dp above the banner. No-banner Clips retain their\n80dp target. Direct/modal Clips retain the 80dp target above the banner, its top-edge track, and 16dp\nof non-clickable separation between lower regions. Eligible Embedded `ABOVE_ACTION` progress also\nretains the 80dp target above the primary action and banner.\n\nHosts that are too short to fit the complete separated stack retain legacy control positions without the new clearance guarantee; the SDK does not hide an eligible banner. 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, the configured synchronous or asynchronous request-builder callback is applied to bottom banner requests as well as native ad requests.\n\nExample setup with bottom banner ads using GAM:\n\n```kotlin\nval storytellerGamModule = StorytellerGamModule.getInstance(applicationContext).apply {\n init(\n adUnit = { storytellerAdRequestInfo: StorytellerAdRequestInfo -> \"/your/ad_unit_id\" },\n bottomBannerAdUnit = { storytellerAdRequestInfo: StorytellerAdRequestInfo -> \"/your/bottom_banner_ad_unit_id\" },\n )\n}\n```\n\nExample setup with bottom banner ads using AdMob:\n\n```kotlin\nval adMobModule = StorytellerAdMobModule.getInstance(applicationContext).apply {\n init(\n nativeAdUnit = { adRequestInfo: StorytellerAdRequestInfo -> \"ca-app-pub-xxx/native\" },\n bottomBannerAdUnit = { adRequestInfo: StorytellerAdRequestInfo -> \"ca-app-pub-xxx/bottom_banner\" },\n )\n}\n```\n\n### Ad Source for Custom Modules\n\nIf your custom `StorytellerModule` provides Google ads, set `googleAdSource` to declare the source used by your implementation.\n\n- Use `StorytellerGoogleAdSource.GAM` for Google Ad Manager modules.\n- Use `StorytellerGoogleAdSource.ADMOB` for Google AdMob modules.\n- Use `null` for non-Google ad providers.\n\nSetting `googleAdSource` enables consistent Google paid ad analytics source values and allows the SDK to normalize multiple Google modules safely.\n\n`StorytellerGamModule` and `StorytellerAdMobModule` set `googleAdSource` automatically.\n\n### Ad Request Information\n\n#### StorytellerAdRequestInfo\n\nThe `StorytellerAdRequestInfo` sealed class has two subclasses:\n\n**`ClipsAdRequestInfo`**\n\nUsed when an Ad is requested for display in a Clips Player. The properties include:\n\n- `collection` - The identifier of the clips collection where the ad will be displayed\n- `nextClipCategories` - List of categories for the next clip associated with the item for ad targeting\n- `adIndex` - Count of the Ad position from the start of the Collection\n- `itemInfo` - Metadata about the item context for ad targeting\n\nFor 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.\n\n**`StoriesAdRequestInfo`**\n\nUsed when an Ad is requested for display in a Stories Player. The properties include:\n\n- `placement` - The identifier of the stories placement where the ad will be displayed\n- `categories` - List of category identifiers for ad targeting and filtering\n- `adIndex` - Count of the Ad position from the start of the Collection\n- `itemInfo` - Metadata about the item context for ad targeting\n\n#### ItemInfo\n\nEach request class includes an `ItemInfo` object that contains:\n\n- `categories` - List of categories associated with the item for ad targeting\n- `contentId` - Identifier of the current Story or Clip, available to dynamic ad callbacks. It is intentionally omitted from `ItemInfo.toString()` so incidental request logging does not expose host content identifiers.\n\n## Non Skippable Ads\n\nOur 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.\n\nFor standard ad-as-Clip placements, the active non-skippable period also blocks switching to Following, moving to the next or previous Clip, and opening category or followable-category navigation. Standard skippable ads retain the existing navigation behavior.\n", "copy_markdown_include_header": false, "base_path": "android", "ai_dir": "ai", "missing_payload_behavior": "empty"}