The Storyteller SDK supports ads created in the Storyteller CMS (First Party
Ads), URL-based ads supplied by your Flutter application, Google Ad Manager
(GAM), Google AdMob, and generic or GAM VAST tags through optional native
integration packages.
Confirm the tenant's ad source, enabled placements and inventory with the
Storyteller Delivery Team before integrating a provider. Registering a module
does not enable a disabled tenant placement or guarantee ad fill.
The installation examples below use published Flutter 12.2.0. Keep the core
and every optional ads package on the same release version. Flutter 12.2.0
uses native Android 12.2.0 and iOS 11.9.1. Features new in a release are
marked below. Native behavior comparisons naming Android 12.1.2 or
iOS 11.8.0 describe the earlier development audit, not the released pins.
See Getting Started for platform setup.
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.
Use the core storyteller_sdk package when your tenant is configured for
Integrating App ads and your application already has an ad service. Register
the provider before initializing Storyteller so it is ready before content can
request an ad:
Supply your ad-service callback as provider. It receives
StorytellerClientAdRequest; use request.context as
StorytellerStoriesAdRequestContext or StorytellerClipsAdRequestContext to
choose targeting for that opportunity. Return null for no fill, or construct
a response from your service's values:
Return StorytellerAd.image(...) or StorytellerAd.video(...). Provide a
non-blank ID, advertiser name and media URL; use valid URLs for media, tracking
and URL-based actions. Image duration defaults to five seconds; video duration
defaults to zero. Durations must be non-negative whole seconds. The provider
timeout defaults to five seconds and accepts 100 milliseconds to two minutes;
adSource defaults to flutter and must not be blank.
Await the returned initialization result and check success before mounting
or reloading Storyteller views. See the first-content example in
Getting Started.
Both native SDKs support web, in-app, store, and external-app actions. Tracking pixels use
the cross-platform events impression, firstQuartile, midpoint,
thirdQuartile, videoComplete, videoPause, and videoResume.
Each callback receives an ephemeral requestId plus typed Stories or Clips
context. The bridge correlates the returned value internally; applications do
not manually complete requests. Concurrent requests are independent. A
null result, callback exception, missing provider, malformed ad, or timeout
fails only that opportunity so native playback can continue. The timeout is
wall-clock time and continues while the app is backgrounded. A response that
arrives after timeout, a player dismissal reported to the bridge,
reconfiguration, or engine detach is ignored safely. Android player paths that
do not expose a dismissal callback remain bounded by the same timeout.
The story, clip and optional nextClip fields use
StorytellerClientAdItemInfo.
Its optional contentId is populated for the current Story or Clip on
Android. In Flutter 12.1.2, the iOS bridge also forwards it
from native 11.8.0; Flutter 11.6.5 does not forward it on iOS.
Android's nextClip is built from category metadata and does not include a
content ID. Treat contentId as optional and do not include it in incidental
logs.
Call Storyteller.setClientAdProvider(null) to disable the provider. This also
fails requests that are still outstanding. Client ads in this bridge are
full-screen Stories and Clips ads; Clips bottom banners require a supported
native Google integration.
The adSource value is attached to native lifecycle analytics. Storyteller's
native SDKs remain responsible for rendering, playback, action handling,
tracking-pixel dispatch, and analytics; do not emit a second lifecycle event
from the Dart callback. Do not log request payloads, ad identifiers, tracking
URLs, or callback exception details. Your application remains responsible for
consent, regional privacy requirements, and the ad service it calls.
Flutter applications may include both GAM and AdMob packages for Google Mobile
Ads, but configure only one facade for a launch. VAST is an independent
optional full-screen provider and can be registered alongside either facade:
Package
Product
Placements and options
storyteller_gam_sdk
Google Ad Manager
Native, custom native templates, full-screen banners, Clips bottom banner, GAM KVPs, and PPID where configured
storyteller_admob_sdk
Google AdMob
Native, fullscreen banner fallback, Clips bottom banner, custom network-extras KVPs, and banner-priority order
storyteller_vast_sdk
Generic or GAM VAST
Full-screen Stories and Clips VAST ads without Google Mobile Ads or IMA; static/dynamic parameters and sanitized diagnostics
The GAM and AdMob packages use Android com.getstoryteller:ads24 and the shared iOS
Storyteller Google ads integration. Calling either package's setup method
replaces an existing GAM or AdMob facade while preserving unrelated modules.
Persist a provider choice and run only that facade's setup before
Storyteller.initialize. Await each setup call, then await initialization and
check its result before presenting content. When using VAST alongside Google
ads, register modules in the intended fallback order; native integrations try
eligible registered modules in order by default. PPID and custom native
templates remain GAM-only features.
Call either package's disable method before initialization to remove the
active GAM or AdMob facade while preserving VAST. This allows an integrating
application Client Ads provider to own eligible tenant inventory:
VAST is isolated in its own package and does not acquire either Google Mobile
Ads artifact. Calling either VAST setup method replaces only an existing VAST
variant and preserves GAM and unrelated modules. Disabling VAST likewise
removes only the module installed by the VAST plugin.
Use storyteller_vast_sdk for vendor-neutral VAST endpoints or GAM VAST tags
when you do not need the Google Mobile Ads or IMA SDKs. It supports full-screen
Stories and Clips ads, including eligible opening pre-roll configured for your
tenant. It does not provide Clips bottom banners.
The HTTPS baseUrl and requestParameters are validated before they cross the
platform channel. The optional provider receives typed Stories or Clips
context. When it completes within one second, dynamic values override static
values with the same key. A missing, throwing, malformed, or timed-out provider
contributes no dynamic values; static values remain and native playback can
continue.
Use StorytellerVASTURLFormat.queryString for standard query parameters or
StorytellerVASTURLFormat.pathSegment when the endpoint expects /key=value
segments. The default is pathSegment; set queryString explicitly for query
endpoints, as in the example. enableDebugLogging defaults to false.
These GAM VAST configuration values are static strings and maps, unlike the
GAM Google Mobile Ads module's ad-unit callbacks. For per-request parameters,
use the generic VAST configuration and its provider.
customParams are encoded into GAM's cust_params. tagParameters are
top-level tag parameters applied after the native defaults. adUnit must not
be blank; description and optional content URLs must use HTTPS.
StorytellerVASTModule.diagnostics is a broadcast stream of sanitized native
request-pipeline events. Depending on the platform and event, its typed model
can include URL format, parameter/candidate/ad counts, elapsed time, VAST
version, wrapper depth, HTTP status, and a stable error category. Raw tag URLs,
parameters, slot/ad/request identifiers, and native error descriptions never
cross the Flutter bridge.
The last generic or GAM VAST setup wins while the selected Google ads facade
remains registered. Provider ordering is supported by Flutter
11.6.5 (Android 11.6.4) and 12.1.2 (Android 12.1.2).
The corresponding iOS 11.6.1 / 11.8.0 baselines support presentation-scoped
pre-roll but not provider order, and the
native GAM VAST pipeline attributes the configured ad unit in ad analytics.
Use StorytellerClipsAdConfiguration on the relevant Clips presentation; see
Additional Methods and Embedded Clips.
An omitted provider order preserves native module order; an empty list disables
standard between-Clip providers on Android. A populated list restricts and
orders eligible providers. iOS ignores that list. Pre-roll and bottom-banner
controls remain subject to tenant configuration.
enableDebugLogging controls verbose native VAST logs on Android; iOS exposes
sanitized diagnostics but no verbose-logging switch.
Dynamic request context is platform-shaped. The top-level Stories placement is
available on Android and iOS; category-level placement metadata is currently
populated on iOS only.
In Flutter 12.1.2, native Android 12.1.2 and iOS 11.8.0
resolve VAST CTA text from a non-blank storyteller:ad-ui extension override,
then the tenant's VAST CTA default, then Learn more. A clickable CTA appears
only when the ad has a click-through destination; CTA text alone cannot create
one. Advertiser presentation likewise uses extension/tenant defaults before
native creative metadata. These are native presentation rules, with no Dart CTA
setting on StorytellerVASTConfiguration.
The native SDKs own fetching, safe XML parsing, wrapper resolution, media
selection, playback, skip behavior, clicks, tracking dispatch, fallback, and
ad lifecycle analytics. Do not reproduce those behaviors or emit duplicate
analytics in Dart. Do not log targeting values, tag URLs, request payloads, or
identifiers. Your application remains responsible for consent, regional
privacy requirements, its VAST endpoint, and the tracking technologies it
selects.
nativeAdUnit is required. bannerAdUnit enables fullscreen fallback; with
the default enableBannerAdPriority: false, Storyteller tries native and then
banner. Set the option to true to try banner and then native.
bottomBannerAdUnit is independent and applies only to tenant-enabled Clips
bottom banners. Use AdMob native-format ad-unit IDs for nativeAdUnit and
banner-format IDs for both banner callbacks (IDs typically use
ca-app-pub-.../...). A full-screen Storyteller banner remains a Google banner;
it is not a Google interstitial, rewarded, or app-open ad unit.
Ad-unit and KVP callbacks may return immediately or asynchronously. Flutter
resolves one configuration snapshot for each native request with a four-second
Dart timeout. Every configured ad-unit callback must return a non-blank string;
a blank, throwing, or timed-out callback fails the ad opportunity so content
playback can continue. customKvps values are sent as AdMob network extras and remain
subject to the host application's consent and regional privacy rules. Native
Storyteller suppresses them when ad tracking is disabled. Do not log ad request
payloads, targeting values, or identifiers. moduleKey and
enableDebugLogging (default true) apply on Android only and are ignored on
iOS. Omit optional banner callbacks when that inventory is unavailable; a blank
configured fallback unit fails the request before either native or banner loads.
The configuration example under packages/storyteller_admob_sdk/example/
uses Google's platform test ad units and excludes the GAM package. It contains
Dart source and a pubspec; supply the generated Android/iOS app projects,
application IDs and a working Storyteller API key before running it. Production applications may include both packages and
select one at startup.
Use StorytellerAdMobModule.disable() to remove the current GAM or AdMob
facade without removing VAST, for example before configuring Client Ads.
To use Google Ad Manager with Storyteller, first contact your Storyteller
representative so the required inventory and custom templates can be enabled
for your tenant.
The storyteller_gam_sdk package configures the native Storyteller GAM modules
on Android and iOS. It supports dynamic ad units, Clips bottom banners, custom
native templates, KVP targeting, Publisher Provided ID (PPID), and host-native
Google request enrichment.
Add the dependency alongside the core SDK. Both packages must use the same
version. The plugin requires Android API 24 or later and iOS 15.0 or later.
It may coexist with storyteller_admob_sdk, provided only the selected facade
is configured for the launch.
Calling GAM setup replaces an existing GAM or AdMob facade while preserving
unrelated modules such as VAST. To switch providers, persist the choice and
configure the selected facade on the next app launch. Pass a host-owned
consentGatedPpid function that reads current consent and returns a current
PPID or null. When PPID is unnecessary, omit publisherProvidedId entirely.
Use GAM ad-unit paths (such as /NETWORK_CODE/AD_UNIT) whose inventory supports
native/custom native or banner formats. The module does not expose a Google
interstitial, rewarded, or app-open loader, or AdMob's banner-priority setting.
Use StorytellerGAMModule.disable() to remove the current GAM or AdMob facade
without removing VAST, for example before configuring Client Ads.
adUnit and bottomBannerAdUnit can return immediately or asynchronously. A
blank, throwing, or timed-out callback skips only that ad opportunity so native
playback can continue. Android resolves the complete request asynchronously in
the Flutter bridge before delegating immutable values to its synchronous native
GAM module, so it does not block the UI thread.
customKvps and publisherProvidedId are synchronous Dart callbacks; only
the GAM ad-unit callbacks and requestCustomKvps can complete asynchronously.
They form one targeting snapshot. The value produced during setup seeds the native cache; requests
refresh the snapshot so it can contain current host state. PPID is trimmed, blank values are omitted,
and the host must return null whenever its consent gate does not permit the
identifier. Pass PPID through its dedicated property rather than a custom KVP.
If a Flutter targeting refresh fails or times out, the bridge may reuse the last
safe KVP map but omits PPID for that request. A previously cached identifier is
never used as a fallback.
requestCustomKvps receives the StorytellerAdRequestInfo of each GAM ad
request, including ads that Storyteller loads ahead of time, so its values can
depend on the Clips collection or Stories list the ad is shown with, for
example when one screen shows collections that need different targeting. Its
values are merged with customKvps and win when both set the same key. The
callback must finish within one second, so derive its values from the request
or from state your app already holds rather than from a network call. If it
throws, that request is sent without its values. If it takes longer, the whole
targeting refresh for that request times out: the request is sent with the last
cached customKvps and without its request values or PPID. The bridge never
reuses another request's requestCustomKvps.
bottomBannerAdUnit is independent and optional. Provide it only when the
tenant has Clips bottom-banner inventory enabled. customNativeTemplateIds
maps Story and Clip custom format IDs supplied by the Storyteller Delivery
team.
moduleKey and enableDebugLogging are Android module options. iOS ignores
them because its native GAM configuration does not expose equivalents.
If you disable ad tracking through StorytellerEventTrackingOptions, native
Storyteller suppresses KVPs and limits ad-related analytics. The PPID callback
still remains host-owned and must enforce the same privacy decision. See
Privacy and Tracking.
APS, Nimbus, and other bidder SDKs mutate native Google request objects, which
cannot cross a Flutter method channel. Register the appropriate native hook
before Dart calls StorytellerGAMModule.setup. Storyteller applies its KVPs,
PPID, and reserved st* targeting before invoking the hook. Preserve those
values, and apply the host application's bidder consent and timeout policy.
Android host code receives the prepared AdManagerAdRequest.Builder and must
call exactly one completion method:
completion.onNoFill() and completion.onFailure() continue with the clean
Storyteller request. Clear the hook with
StorytellerGAMModulePlugin.clearAdRequestEnricher().
iOS host code receives the prepared Google Request in an async main-actor
closure:
GAM and AdMob each export their own StoriesAdRequestInfo,
ClipsAdRequestInfo, ItemInfo and category types. Prefix imports when using
both packages so their same-named types remain distinguishable. VAST and Client
Ads use their separately named context types shown above.
Storyteller surfaces detailed context for every ad opportunity:
StoriesAdRequestInfo
placement – native placement code configured in Storyteller Studio.
categories – list of category IDs for the Stories list being displayed; use
story.categories for the current Story's category metadata.
story – ItemInfo describing the story and its categories.
adIndex – native index of the ad opportunity (full-screen ad indices start at one).
ClipsAdRequestInfo
collection – collection ID currently being viewed.
clip – ItemInfo for the active clip.
nextClip – optional ItemInfo for the clip that will follow.
adIndex – native index of the ad opportunity (full-screen ad indices start at one).
Treat adIndex as context supplied by native playback, not as an array offset
or a promise that an ad loaded. ItemInfo in GAM, AdMob and VAST exposes category metadata and, from
Flutter 12.1.2, the optional contentId of the current Story or Clip, as
Client Ads does. On Android, nextClip carries category metadata only. Treat
contentId as optional and do not include it in incidental logs.
Category-level placement metadata is populated on iOS only in these bridges.
Use this metadata to choose an ad unit or drive native bidder logic. Do not
include request payloads, targeting values, PPIDs, or ad identifiers in logs.
Set enableDebugLogging to true (the default) while integrating on Android;
switch it off for production if the extra module logging is not required.
Dart callback diagnostics omit values, but verbose native module logs can
contain ad-unit IDs and targeting; do not forward raw logs to analytics or
shared support channels.
Symptom
Check next
Ads never request
Confirm successful initialization, tenant ad source and placement settings; await provider setup before playback.
A Google ads app crashes before playback
Verify the platform application ID below; an application ID is different from an ad-unit ID.
Native ads work but fallback banners do not
Check banner-format inventory and the configured callback. For AdMob, a blank configured banner unit fails the whole request; omit an unavailable fallback.
Clips bottom banner never appears
Confirm tenant bottom banners are enabled, configure bottomBannerAdUnit, and check presentation bottomBannerEnabled; Client Ads and VAST do not provide it.
Custom KVPs are absent
Check StorytellerEventTrackingOptions.enableAdTracking and the host consent gate; native GAM/AdMob suppress their KVPs when ad tracking is disabled.
VAST requests fail or have no dynamic values
Check HTTPS URLs, non-blank string parameters, endpoint URL format, and the one-second provider deadline; inspect sanitized diagnostics for request, parse or media-selection failure.
VAST CTA is absent
Confirm the creative has a click-through URL; CTA copy does not supply a destination.
A provider change appears ineffective
Verify only the selected GAM/AdMob setup runs. Later setup replaces the Google facade; VAST setup replaces only its own variant.
No fill can be a valid inventory response. Successful module setup is not proof
that an ad will be served. See Troubleshooting for the
initialization and content checks before investigating ad inventory.
Application IDs use a ~ separator; ad-unit IDs are supplied to module
callbacks instead. Follow Google's platform configuration and measurement
instructions for Android GAM,
iOS GAM,
or AdMob app configuration.
The Storyteller integration brings its native Google dependency; it does not
require adding the separate google_mobile_ads Flutter package to configure
Storyteller ads. Generic/GAM VAST alone does not require these application IDs.
{"slug": "ads", "page_title": "Ads", "page_url": "Ads/", "canonical_url": "/flutter/Ads/", "markdown": "# Ads\n\n## Introduction\n\nThe Storyteller SDK supports ads created in the Storyteller CMS (First Party\nAds), URL-based ads supplied by your Flutter application, Google Ad Manager\n(GAM), Google AdMob, and generic or GAM VAST tags through optional native\nintegration packages.\n\nConfirm the tenant's ad source, enabled placements and inventory with the\nStoryteller Delivery Team before integrating a provider. Registering a module\ndoes not enable a disabled tenant placement or guarantee ad fill.\n\nThe installation examples below use published Flutter `12.2.0`. Keep the core\nand every optional ads package on the same release version. Flutter `12.2.0`\nuses native Android `12.2.0` and iOS `11.9.1`. Features new in a release are\nmarked below. Native behavior comparisons naming Android `12.1.2` or\niOS `11.8.0` describe the earlier development audit, not the released pins.\nSee [Getting Started](getting-started/index.md) for platform setup.\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 entirely within the Storyteller SDK.\n\n## Host-supplied client ads\n\nUse the core `storyteller_sdk` package when your tenant is configured for\n**Integrating App** ads and your application already has an ad service. Register\nthe provider before initializing Storyteller so it is ready before content can\nrequest an ad:\n\n```dart\nimport 'package:storyteller_sdk/storyteller_sdk.dart';\n\nFuture<StorytellerInitializeResult> configureClientAds({\n required StorytellerClientAdProvider provider,\n}) async {\n await Storyteller.setClientAdProvider(\n provider,\n adSource: 'YOUR_AD_SOURCE',\n timeout: const Duration(seconds: 8),\n );\n\n return Storyteller.initialize('YOUR_API_KEY');\n}\n```\n\nSupply your ad-service callback as `provider`. It receives\n`StorytellerClientAdRequest`; use `request.context` as\n`StorytellerStoriesAdRequestContext` or `StorytellerClipsAdRequestContext` to\nchoose targeting for that opportunity. Return `null` for no fill, or construct\na response from your service's values:\n\n```dart\nStorytellerAd videoAdFromService({\n required String id,\n required String advertiserName,\n required String videoUrl,\n required String impressionUrl,\n required String completionUrl,\n required String destination,\n required String actionText,\n int durationSeconds = 0,\n String? playcardUrl,\n}) {\n return StorytellerAd.video(\n id: id,\n advertiserName: advertiserName,\n videoUrl: videoUrl,\n durationSeconds: durationSeconds,\n playcardUrl: playcardUrl,\n trackingPixels: [\n StorytellerAdTrackingPixel(url: impressionUrl),\n StorytellerAdTrackingPixel(\n event: StorytellerAdTrackingEvent.videoComplete,\n url: completionUrl,\n ),\n ],\n action: StorytellerAdAction(\n type: StorytellerAdActionType.externalApp,\n urlOrStoreId: destination,\n text: actionText,\n ),\n );\n}\n```\n\nReturn `StorytellerAd.image(...)` or `StorytellerAd.video(...)`. Provide a\nnon-blank ID, advertiser name and media URL; use valid URLs for media, tracking\nand URL-based actions. Image duration defaults to five seconds; video duration\ndefaults to zero. Durations must be non-negative whole seconds. The provider\ntimeout defaults to five seconds and accepts 100 milliseconds to two minutes;\n`adSource` defaults to `flutter` and must not be blank.\n\nAwait the returned initialization result and check `success` before mounting\nor reloading Storyteller views. See the first-content example in\n[Getting Started](getting-started/index.md).\n\nBoth native SDKs support web, in-app, store, and external-app actions. Tracking pixels use\nthe cross-platform events `impression`, `firstQuartile`, `midpoint`,\n`thirdQuartile`, `videoComplete`, `videoPause`, and `videoResume`.\n\nEach callback receives an ephemeral `requestId` plus typed Stories or Clips\ncontext. The bridge correlates the returned value internally; applications do\nnot manually complete requests. Concurrent requests are independent. A\n`null` result, callback exception, missing provider, malformed ad, or timeout\nfails only that opportunity so native playback can continue. The timeout is\nwall-clock time and continues while the app is backgrounded. A response that\narrives after timeout, a player dismissal reported to the bridge,\nreconfiguration, or engine detach is ignored safely. Android player paths that\ndo not expose a dismissal callback remain bounded by the same timeout.\n\nThe `story`, `clip` and optional `nextClip` fields use\n`StorytellerClientAdItemInfo`.\nIts optional `contentId` is populated for the current Story or Clip on\nAndroid. In Flutter `12.1.2`, the iOS bridge also forwards it\nfrom native `11.8.0`; Flutter `11.6.5` does not forward it on iOS.\nAndroid's `nextClip` is built from category metadata and does not include a\ncontent ID. Treat `contentId` as optional and do not include it in incidental\nlogs.\n\nCall `Storyteller.setClientAdProvider(null)` to disable the provider. This also\nfails requests that are still outstanding. Client ads in this bridge are\nfull-screen Stories and Clips ads; Clips bottom banners require a supported\nnative Google integration.\n\nThe `adSource` value is attached to native lifecycle analytics. Storyteller's\nnative SDKs remain responsible for rendering, playback, action handling,\ntracking-pixel dispatch, and analytics; do not emit a second lifecycle event\nfrom the Dart callback. Do not log request payloads, ad identifiers, tracking\nURLs, or callback exception details. Your application remains responsible for\nconsent, regional privacy requirements, and the ad service it calls.\n\n## Choose the active native ads facade\n\nFlutter applications may include both GAM and AdMob packages for Google Mobile\nAds, but configure only one facade for a launch. VAST is an independent\noptional full-screen provider and can be registered alongside either facade:\n\n| Package | Product | Placements and options |\n| --- | --- | --- |\n| `storyteller_gam_sdk` | Google Ad Manager | Native, custom native templates, full-screen banners, Clips bottom banner, GAM KVPs, and PPID where configured |\n| `storyteller_admob_sdk` | Google AdMob | Native, fullscreen banner fallback, Clips bottom banner, custom network-extras KVPs, and banner-priority order |\n| `storyteller_vast_sdk` | Generic or GAM VAST | Full-screen Stories and Clips VAST ads without Google Mobile Ads or IMA; static/dynamic parameters and sanitized diagnostics |\n\nThe GAM and AdMob packages use Android `com.getstoryteller:ads24` and the shared iOS\nStoryteller Google ads integration. Calling either package's `setup` method\nreplaces an existing GAM or AdMob facade while preserving unrelated modules.\nPersist a provider choice and run only that facade's setup before\n`Storyteller.initialize`. Await each setup call, then await initialization and\ncheck its result before presenting content. When using VAST alongside Google\nads, register modules in the intended fallback order; native integrations try\neligible registered modules in order by default. PPID and custom native\ntemplates remain GAM-only features.\n\nCall either package's `disable` method before initialization to remove the\nactive GAM or AdMob facade while preserving VAST. This allows an integrating\napplication Client Ads provider to own eligible tenant inventory:\n\n```dart\nawait StorytellerGAMModule.disable();\n// Or: await StorytellerAdMobModule.disable();\n```\n\nVAST is isolated in its own package and does not acquire either Google Mobile\nAds artifact. Calling either VAST setup method replaces only an existing VAST\nvariant and preserves GAM and unrelated modules. Disabling VAST likewise\nremoves only the module installed by the VAST plugin.\n\n## Storyteller VAST SDK\n\nUse `storyteller_vast_sdk` for vendor-neutral VAST endpoints or GAM VAST tags\nwhen you do not need the Google Mobile Ads or IMA SDKs. It supports full-screen\nStories and Clips ads, including eligible opening pre-roll configured for your\ntenant. It does not provide Clips bottom banners.\n\nAdd it alongside the core package:\n\n```yaml\ndependencies:\n storyteller_sdk: ^12.2.0\n storyteller_vast_sdk: ^12.2.0\n```\n\nThe plugin requires Android API 24 or later and iOS 15.0 or later. Configure it\nbefore `Storyteller.initialize` so ads are ready before playback starts.\n\n### Generic VAST setup\n\n```dart\nimport 'package:storyteller_sdk/storyteller_sdk.dart';\nimport 'package:storyteller_vast_sdk/storyteller_vast_sdk.dart';\n\nFuture<StorytellerInitializeResult> configureStoryteller() async {\n await StorytellerVASTModule.setupVAST(\n StorytellerVASTConfiguration(\n baseUrl: 'https://ads.example.com/vast',\n requestParameters: const {'output': 'vast', 'environment': 'production'},\n requestParametersProvider: (request) {\n return {\n 'content_type': switch (request) {\n StorytellerVASTStoriesAdRequestInfo() => 'story',\n StorytellerVASTClipsAdRequestInfo() => 'clip',\n },\n };\n },\n urlFormat: StorytellerVASTURLFormat.queryString,\n ),\n );\n\n return Storyteller.initialize('YOUR_API_KEY');\n}\n```\n\nThe HTTPS `baseUrl` and `requestParameters` are validated before they cross the\nplatform channel. The optional provider receives typed Stories or Clips\ncontext. When it completes within one second, dynamic values override static\nvalues with the same key. A missing, throwing, malformed, or timed-out provider\ncontributes no dynamic values; static values remain and native playback can\ncontinue.\n\nUse `StorytellerVASTURLFormat.queryString` for standard query parameters or\n`StorytellerVASTURLFormat.pathSegment` when the endpoint expects `/key=value`\nsegments. The default is `pathSegment`; set `queryString` explicitly for query\nendpoints, as in the example. `enableDebugLogging` defaults to `false`.\n\n### GAM VAST setup\n\nGAM VAST constructs a Google VAST tag without adding Google Mobile Ads or IMA:\n\n```dart\nawait StorytellerVASTModule.setupGAMVAST(\n StorytellerGAMVASTConfiguration(\n adUnit: '/YOUR_NETWORK/YOUR_AD_UNIT',\n descriptionUrl: 'https://www.example.com/player',\n contentUrl: 'https://www.example.com/content/123',\n customParams: const {'section': 'sport'},\n tagParameters: const {\n 'sz': '640x480',\n 'unviewed_position_start': '1',\n },\n ),\n);\n```\n\nThese GAM VAST configuration values are static strings and maps, unlike the\nGAM Google Mobile Ads module's ad-unit callbacks. For per-request parameters,\nuse the generic VAST configuration and its provider.\n\n`customParams` are encoded into GAM's `cust_params`. `tagParameters` are\ntop-level tag parameters applied after the native defaults. `adUnit` must not\nbe blank; description and optional content URLs must use HTTPS.\n\n### Diagnostics, replacement, and privacy\n\n`StorytellerVASTModule.diagnostics` is a broadcast stream of sanitized native\nrequest-pipeline events. Depending on the platform and event, its typed model\ncan include URL format, parameter/candidate/ad counts, elapsed time, VAST\nversion, wrapper depth, HTTP status, and a stable error category. Raw tag URLs,\nparameters, slot/ad/request identifiers, and native error descriptions never\ncross the Flutter bridge.\n\n```dart\nimport 'package:flutter/foundation.dart';\nimport 'package:storyteller_vast_sdk/storyteller_vast_sdk.dart';\n\nfinal subscription = StorytellerVASTModule.diagnostics.listen((event) {\n debugPrint('VAST event: ${event.type.value}');\n});\n\nawait StorytellerVASTModule.disable();\nawait subscription.cancel();\n```\n\nThe last generic or GAM VAST setup wins while the selected Google ads facade\nremains registered. Provider ordering is supported by Flutter\n`11.6.5` (Android `11.6.4`) and `12.1.2` (Android `12.1.2`).\nThe corresponding iOS `11.6.1` / `11.8.0` baselines support presentation-scoped\npre-roll but not provider order, and the\nnative GAM VAST pipeline attributes the configured ad unit in ad analytics.\nUse `StorytellerClipsAdConfiguration` on the relevant Clips presentation; see\n[Additional Methods](AdditionalMethods.md) and [Embedded Clips](EmbeddedClips.md).\nAn omitted provider order preserves native module order; an empty list disables\nstandard between-Clip providers on Android. A populated list restricts and\norders eligible providers. iOS ignores that list. Pre-roll and bottom-banner\ncontrols remain subject to tenant configuration.\n`enableDebugLogging` controls verbose native VAST logs on Android; iOS exposes\nsanitized diagnostics but no verbose-logging switch.\n\nDynamic request context is platform-shaped. The top-level Stories placement is\navailable on Android and iOS; category-level `placement` metadata is currently\npopulated on iOS only.\n\nIn Flutter `12.1.2`, native Android `12.1.2` and iOS `11.8.0`\nresolve VAST CTA text from a non-blank `storyteller:ad-ui` extension override,\nthen the tenant's VAST CTA default, then `Learn more`. A clickable CTA appears\nonly when the ad has a click-through destination; CTA text alone cannot create\none. Advertiser presentation likewise uses extension/tenant defaults before\nnative creative metadata. These are native presentation rules, with no Dart CTA\nsetting on `StorytellerVASTConfiguration`.\n\nThe native SDKs own fetching, safe XML parsing, wrapper resolution, media\nselection, playback, skip behavior, clicks, tracking dispatch, fallback, and\nad lifecycle analytics. Do not reproduce those behaviors or emit duplicate\nanalytics in Dart. Do not log targeting values, tag URLs, request payloads, or\nidentifiers. Your application remains responsible for consent, regional\nprivacy requirements, its VAST endpoint, and the tracking technologies it\nselects.\n\n## Storyteller AdMob SDK\n\nAdd the AdMob package alongside the core SDK:\n\n```yaml\ndependencies:\n storyteller_sdk: ^12.2.0\n storyteller_admob_sdk: ^12.2.0\n```\n\nThe plugin requires Android API 24 or later and iOS 15.0 or later. Complete the\n[Google Mobile Ads application setup](#google-mobile-ads-application-setup)\nbelow before running AdMob.\n\nRegister AdMob before calling `Storyteller.initialize`, so the native module is\navailable before content can request an ad:\n\n```dart\nimport 'package:storyteller_admob_sdk/storyteller_admob_sdk.dart';\nimport 'package:storyteller_sdk/storyteller_sdk.dart';\n\nFuture<StorytellerInitializeResult> configureStoryteller() async {\n await StorytellerAdMobModule.setup(\n StorytellerAdMobModuleConfiguration(\n nativeAdUnit: (StorytellerAdRequestInfo request) {\n if (request is StoriesAdRequestInfo) {\n return 'YOUR_STORIES_NATIVE_AD_UNIT';\n }\n return 'YOUR_CLIPS_NATIVE_AD_UNIT';\n },\n bannerAdUnit: (request) => 'YOUR_FULLSCREEN_BANNER_AD_UNIT',\n bottomBannerAdUnit: (request) => 'YOUR_CLIPS_BOTTOM_BANNER_AD_UNIT',\n customKvps: () => {'app_section': 'storyteller'},\n enableBannerAdPriority: false,\n ),\n );\n\n return Storyteller.initialize('YOUR_API_KEY');\n}\n```\n\n`nativeAdUnit` is required. `bannerAdUnit` enables fullscreen fallback; with\nthe default `enableBannerAdPriority: false`, Storyteller tries native and then\nbanner. Set the option to `true` to try banner and then native.\n`bottomBannerAdUnit` is independent and applies only to tenant-enabled Clips\nbottom banners. Use AdMob native-format ad-unit IDs for `nativeAdUnit` and\nbanner-format IDs for both banner callbacks (IDs typically use\n`ca-app-pub-.../...`). A full-screen Storyteller banner remains a Google banner;\nit is not a Google interstitial, rewarded, or app-open ad unit.\n\nAd-unit and KVP callbacks may return immediately or asynchronously. Flutter\nresolves one configuration snapshot for each native request with a four-second\nDart timeout. Every configured ad-unit callback must return a non-blank string;\na blank, throwing, or timed-out callback fails the ad opportunity so content\nplayback can continue. `customKvps` values are sent as AdMob network extras and remain\nsubject to the host application's consent and regional privacy rules. Native\nStoryteller suppresses them when ad tracking is disabled. Do not log ad request\npayloads, targeting values, or identifiers. `moduleKey` and\n`enableDebugLogging` (default `true`) apply on Android only and are ignored on\niOS. Omit optional banner callbacks when that inventory is unavailable; a blank\nconfigured fallback unit fails the request before either native or banner loads.\n\nThe configuration example under `packages/storyteller_admob_sdk/example/`\nuses Google's platform test ad units and excludes the GAM package. It contains\nDart source and a pubspec; supply the generated Android/iOS app projects,\napplication IDs and a working Storyteller API key before running it. Production applications may include both packages and\nselect one at startup.\n\nUse `StorytellerAdMobModule.disable()` to remove the current GAM or AdMob\nfacade without removing VAST, for example before configuring Client Ads.\n\n## Storyteller GAM SDK\n\nTo use Google Ad Manager with Storyteller, first contact your Storyteller\nrepresentative so the required inventory and custom templates can be enabled\nfor your tenant.\n\nThe `storyteller_gam_sdk` package configures the native Storyteller GAM modules\non Android and iOS. It supports dynamic ad units, Clips bottom banners, custom\nnative templates, KVP targeting, Publisher Provided ID (PPID), and host-native\nGoogle request enrichment.\n\n### Basic Setup\n\nAdd the dependency alongside the core SDK. Both packages must use the same\nversion. The plugin requires Android API 24 or later and iOS 15.0 or later.\nIt may coexist with `storyteller_admob_sdk`, provided only the selected facade\nis configured for the launch.\n\n```yaml\ndependencies:\n storyteller_sdk: ^12.2.0\n storyteller_gam_sdk: ^12.2.0\n```\n\nComplete the [Google Mobile Ads application setup](#google-mobile-ads-application-setup)\nbelow for GAM as well. Then install the dependencies:\n\n```bash\nflutter pub get\n```\n\n### Configure the GAM module\n\nCall `StorytellerGAMModule.setup` before `Storyteller.initialize`. Provide a\nmain ad unit for every request. All other values are optional.\n\n```dart\nimport 'package:storyteller_gam_sdk/storyteller_gam_module.dart';\nimport 'package:storyteller_sdk/storyteller_sdk.dart';\n\nFuture<StorytellerInitializeResult> configureGam({\n required String? Function() consentGatedPpid,\n}) async {\n await StorytellerGAMModule.setup(\n StorytellerGAMModuleConfiguration(\n adUnit: (StorytellerAdRequestInfo request) async {\n if (request is StoriesAdRequestInfo) {\n return '/YOUR_STORIES_AD_UNIT_ID';\n }\n if (request is ClipsAdRequestInfo) {\n return '/YOUR_CLIPS_AD_UNIT_ID';\n }\n return '/YOUR_NETWORK/YOUR_DEFAULT_AD_UNIT_ID';\n },\n bottomBannerAdUnit: (_) async => '/YOUR_CLIPS_BOTTOM_BANNER_AD_UNIT_ID',\n customNativeTemplateIds: const StorytellerCustomNativeTemplateIds(\n stories: 'YOUR_STORIES_TEMPLATE_ID',\n clips: 'YOUR_CLIPS_TEMPLATE_ID',\n ),\n customKvps: () => {'appmode': 'prod'},\n requestCustomKvps: (StorytellerAdRequestInfo request) => {\n if (request is ClipsAdRequestInfo) 'collection': request.collection,\n if (request is StoriesAdRequestInfo)\n 'categories': request.categories.join(','),\n },\n publisherProvidedId: consentGatedPpid,\n enableDebugLogging: true,\n ),\n );\n\n return Storyteller.initialize('YOUR_API_KEY');\n}\n```\n\nCalling GAM setup replaces an existing GAM or AdMob facade while preserving\nunrelated modules such as VAST. To switch providers, persist the choice and\nconfigure the selected facade on the next app launch. Pass a host-owned\n`consentGatedPpid` function that reads current consent and returns a current\nPPID or `null`. When PPID is unnecessary, omit `publisherProvidedId` entirely.\nUse GAM ad-unit paths (such as `/NETWORK_CODE/AD_UNIT`) whose inventory supports\nnative/custom native or banner formats. The module does not expose a Google\ninterstitial, rewarded, or app-open loader, or AdMob's banner-priority setting.\n\nUse `StorytellerGAMModule.disable()` to remove the current GAM or AdMob facade\nwithout removing VAST, for example before configuring Client Ads.\n\n`adUnit` and `bottomBannerAdUnit` can return immediately or asynchronously. A\nblank, throwing, or timed-out callback skips only that ad opportunity so native\nplayback can continue. Android resolves the complete request asynchronously in\nthe Flutter bridge before delegating immutable values to its synchronous native\nGAM module, so it does not block the UI thread.\n\n`customKvps` and `publisherProvidedId` are synchronous Dart callbacks; only\nthe GAM ad-unit callbacks and `requestCustomKvps` can complete asynchronously.\nThey form one targeting snapshot. The value produced during setup seeds the native cache; requests\nrefresh the snapshot so it can contain current host state. PPID is trimmed, blank values are omitted,\nand the host must return `null` whenever its consent gate does not permit the\nidentifier. Pass PPID through its dedicated property rather than a custom KVP.\n\nIf a Flutter targeting refresh fails or times out, the bridge may reuse the last\nsafe KVP map but omits PPID for that request. A previously cached identifier is\nnever used as a fallback.\n\n`requestCustomKvps` receives the `StorytellerAdRequestInfo` of each GAM ad\nrequest, including ads that Storyteller loads ahead of time, so its values can\ndepend on the Clips collection or Stories list the ad is shown with, for\nexample when one screen shows collections that need different targeting. Its\nvalues are merged with `customKvps` and win when both set the same key. The\ncallback must finish within one second, so derive its values from the request\nor from state your app already holds rather than from a network call. If it\nthrows, that request is sent without its values. If it takes longer, the whole\ntargeting refresh for that request times out: the request is sent with the last\ncached `customKvps` and without its request values or PPID. The bridge never\nreuses another request's `requestCustomKvps`.\n\n`bottomBannerAdUnit` is independent and optional. Provide it only when the\ntenant has Clips bottom-banner inventory enabled. `customNativeTemplateIds`\nmaps Story and Clip custom format IDs supplied by the Storyteller Delivery\nteam.\n\n`moduleKey` and `enableDebugLogging` are Android module options. iOS ignores\nthem because its native GAM configuration does not expose equivalents.\n\nIf you disable ad tracking through `StorytellerEventTrackingOptions`, native\nStoryteller suppresses KVPs and limits ad-related analytics. The PPID callback\nstill remains host-owned and must enforce the same privacy decision. See\n[Privacy and Tracking](PrivacyAndTracking.md).\n\n### Native request enrichment\n\nAPS, Nimbus, and other bidder SDKs mutate native Google request objects, which\ncannot cross a Flutter method channel. Register the appropriate native hook\nbefore Dart calls `StorytellerGAMModule.setup`. Storyteller applies its KVPs,\nPPID, and reserved `st*` targeting before invoking the hook. Preserve those\nvalues, and apply the host application's bidder consent and timeout policy.\n\nAndroid host code receives the prepared `AdManagerAdRequest.Builder` and must\ncall exactly one completion method:\n\n```kotlin\nimport com.storyteller.gam.sdk.flutter.StorytellerGAMModulePlugin\n\nStorytellerGAMModulePlugin.registerAdRequestEnricher {\n requestInfo, builder, completion ->\n bidder.load(requestInfo) { targeting ->\n targeting?.let { builder.addCustomTargeting(\"bidder_key\", it) }\n completion.onSuccess(builder)\n }\n}\n```\n\n`completion.onNoFill()` and `completion.onFailure()` continue with the clean\nStoryteller request. Clear the hook with\n`StorytellerGAMModulePlugin.clearAdRequestEnricher()`.\n\niOS host code receives the prepared Google `Request` in an async main-actor\nclosure:\n\n```swift\nimport storyteller_gam_sdk\n\nStorytellerGAMModulePlugin.registerAdRequestConfiguration { requestInfo, request in\n let targeting = await bidder.targeting(for: requestInfo)\n var customTargeting = request.customTargeting ?? [:]\n customTargeting[\"bidder_key\"] = targeting\n request.customTargeting = customTargeting\n}\n```\n\nClear it with\n`StorytellerGAMModulePlugin.clearAdRequestConfiguration()`.\n\n### Understanding ad request payloads\n\nGAM and AdMob each export their own `StoriesAdRequestInfo`,\n`ClipsAdRequestInfo`, `ItemInfo` and category types. Prefix imports when using\nboth packages so their same-named types remain distinguishable. VAST and Client\nAds use their separately named context types shown above.\n\nStoryteller surfaces detailed context for every ad opportunity:\n\n- `StoriesAdRequestInfo`\n - `placement` \u2013 native placement code configured in Storyteller Studio.\n - `categories` \u2013 list of category IDs for the Stories list being displayed; use\n `story.categories` for the current Story's category metadata.\n - `story` \u2013 `ItemInfo` describing the story and its categories.\n - `adIndex` \u2013 native index of the ad opportunity (full-screen ad indices start at one).\n- `ClipsAdRequestInfo`\n - `collection` \u2013 collection ID currently being viewed.\n - `clip` \u2013 `ItemInfo` for the active clip.\n - `nextClip` \u2013 optional `ItemInfo` for the clip that will follow.\n - `adIndex` \u2013 native index of the ad opportunity (full-screen ad indices start at one).\n\nTreat `adIndex` as context supplied by native playback, not as an array offset\nor a promise that an ad loaded. `ItemInfo` in GAM, AdMob and VAST exposes category metadata and, from\nFlutter `12.1.2`, the optional `contentId` of the current Story or Clip, as\nClient Ads does. On Android, `nextClip` carries category metadata only. Treat\n`contentId` as optional and do not include it in incidental logs.\nCategory-level `placement` metadata is populated on iOS only in these bridges.\n\nUse this metadata to choose an ad unit or drive native bidder logic. Do not\ninclude request payloads, targeting values, PPIDs, or ad identifiers in logs.\n\n### Logging and troubleshooting\n\nSet `enableDebugLogging` to `true` (the default) while integrating on Android;\nswitch it off for production if the extra module logging is not required.\nDart callback diagnostics omit values, but verbose native module logs can\ncontain ad-unit IDs and targeting; do not forward raw logs to analytics or\nshared support channels.\n\n| Symptom | Check next |\n| --- | --- |\n| Ads never request | Confirm successful initialization, tenant ad source and placement settings; await provider setup before playback. |\n| A Google ads app crashes before playback | Verify the platform application ID below; an application ID is different from an ad-unit ID. |\n| Native ads work but fallback banners do not | Check banner-format inventory and the configured callback. For AdMob, a blank configured banner unit fails the whole request; omit an unavailable fallback. |\n| Clips bottom banner never appears | Confirm tenant bottom banners are enabled, configure `bottomBannerAdUnit`, and check presentation `bottomBannerEnabled`; Client Ads and VAST do not provide it. |\n| Custom KVPs are absent | Check `StorytellerEventTrackingOptions.enableAdTracking` and the host consent gate; native GAM/AdMob suppress their KVPs when ad tracking is disabled. |\n| VAST requests fail or have no dynamic values | Check HTTPS URLs, non-blank string parameters, endpoint URL format, and the one-second provider deadline; inspect sanitized diagnostics for request, parse or media-selection failure. |\n| VAST CTA is absent | Confirm the creative has a click-through URL; CTA copy does not supply a destination. |\n| A provider change appears ineffective | Verify only the selected GAM/AdMob setup runs. Later setup replaces the Google facade; VAST setup replaces only its own variant. |\n\nNo fill can be a valid inventory response. Successful module setup is not proof\nthat an ad will be served. See [Troubleshooting](Troubleshooting.md) for the\ninitialization and content checks before investigating ad inventory.\n\nThe Showcase app centralises GAM setup inside\n[`StorytellerService._configureGamModule`](https://github.com/getstoryteller/storyteller-showcase-flutter/blob/main/lib/services/storyteller_service.dart#L188).\n\n### Google Mobile Ads application setup\n\nFor GAM or AdMob, add the Google Mobile Ads **application ID** for each platform.\nIn `android/app/src/main/AndroidManifest.xml`, inside `<application>`:\n\n```xml\n<meta-data\n android:name=\"com.google.android.gms.ads.APPLICATION_ID\"\n android:value=\"YOUR_GOOGLE_MOBILE_ADS_APPLICATION_ID\" />\n```\n\nIn `ios/Runner/Info.plist`:\n\n```xml\n<key>GADApplicationIdentifier<\/key>\n<string>YOUR_GOOGLE_MOBILE_ADS_APPLICATION_ID<\/string>\n```\n\nApplication IDs use a `~` separator; ad-unit IDs are supplied to module\ncallbacks instead. Follow Google's platform configuration and measurement\ninstructions for [Android GAM](https://developers.google.com/ad-manager/mobile-ads-sdk/android/quick-start),\n[iOS GAM](https://developers.google.com/ad-manager/mobile-ads-sdk/ios/quick-start),\nor [AdMob app configuration](https://developers.google.com/admob/flutter/quick-start#configure_your_app).\nThe Storyteller integration brings its native Google dependency; it does not\nrequire adding the separate `google_mobile_ads` Flutter package to configure\nStoryteller ads. Generic/GAM VAST alone does not require these application IDs.\n", "copy_markdown_include_header": false, "base_path": "", "ai_dir": "ai", "missing_payload_behavior": "empty"}