Ads#
Introduction#
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.
Which source of ads is used can be configured on your behalf by a member of the Storyteller Delivery Team.
Storyteller First Party Ads#
If your tenant is configured to use Storyteller First Party Ads, which can be managed in the Storyteller CMS, then no changes to the Storyteller integration code are necessary. The Ads code is managed entirely within the Storyteller SDK.
Host-supplied client ads#
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:
import 'package:storyteller_sdk/storyteller_sdk.dart';
Future<void> configureStoryteller() async {
await Storyteller.setClientAdProvider(
(StorytellerClientAdRequest request) async {
final context = request.context;
if (context is StorytellerStoriesAdRequestContext) {
// Use context.placement, context.categories, context.story, and
// context.adIndex to request an ad from your service.
} else if (context is StorytellerClipsAdRequestContext) {
// Use context.collection, context.clip, context.nextClip, and
// context.adIndex.
}
final response = await yourAdService.loadAd(context);
if (response == null) return null; // No fill.
return StorytellerAd.video(
id: response.id,
advertiserName: response.advertiserName,
videoUrl: response.videoUrl,
durationSeconds: response.durationSeconds,
playcardUrl: response.playcardUrl,
trackingPixels: [
StorytellerAdTrackingPixel(
url: response.impressionUrl,
),
StorytellerAdTrackingPixel(
event: StorytellerAdTrackingEvent.videoComplete,
url: response.completionUrl,
),
],
action: StorytellerAdAction(
type: StorytellerAdActionType.externalApp,
urlOrStoreId: response.destination,
text: response.actionText,
),
);
},
adSource: 'YOUR_AD_SOURCE',
timeout: const Duration(seconds: 8),
);
await Storyteller.initialize('YOUR_API_KEY');
}
Return StorytellerAd.image(...) or StorytellerAd.video(...). 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 current, next, and placement items use StorytellerClientAdItemInfo.
Its optional contentId is populated by Android Storyteller 11.6.3 when native
content provides it. It remains absent on iOS 11.6.1 because that native field
is not public. Treat it 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.
Choose the active native ads facade#
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 v24 | Native, custom native templates, fullscreen banner fallback, 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 |
Both 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. 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:
await StorytellerGAMModule.disable();
// Or: await StorytellerAdMobModule.disable();
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.
Storyteller VAST SDK#
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.
Add it alongside the core package:
dependencies:
storyteller_sdk: ^11.6.5
storyteller_vast_sdk: ^11.6.5
The plugin requires Android API 24 or later and iOS 15.0 or later. Configure it
before Storyteller.initialize so ads are ready before playback starts.
Generic VAST setup#
import 'package:storyteller_sdk/storyteller_sdk.dart';
import 'package:storyteller_vast_sdk/storyteller_vast_sdk.dart';
Future<void> configureStoryteller() async {
await StorytellerVASTModule.setupVAST(
StorytellerVASTConfiguration(
baseUrl: 'https://ads.example.com/vast',
requestParameters: const {
'output': 'vast',
'environment': 'production',
},
requestParametersProvider: (request) {
return {
'content_type': switch (request) {
StorytellerVASTStoriesAdRequestInfo() => 'story',
StorytellerVASTClipsAdRequestInfo() => 'clip',
},
};
},
urlFormat: StorytellerVASTURLFormat.queryString,
),
);
await Storyteller.initialize('YOUR_API_KEY');
}
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.
GAM VAST setup#
GAM VAST constructs a Google VAST tag without adding Google Mobile Ads or IMA:
await StorytellerVASTModule.setupGAMVAST(
StorytellerGAMVASTConfiguration(
adUnit: '/YOUR_NETWORK/YOUR_AD_UNIT',
descriptionUrl: 'https://www.example.com/player',
contentUrl: 'https://www.example.com/content/123',
customParams: const {'section': 'sport'},
tagParameters: const {
'sz': '640x480',
'unviewed_position_start': '1',
},
),
);
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.
Diagnostics, replacement, and privacy#
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.
final subscription = StorytellerVASTModule.diagnostics.listen((event) {
debugPrint('VAST event: ${event.type.value}');
});
await StorytellerVASTModule.disable();
await subscription.cancel();
The last generic or GAM VAST setup wins while the selected Google ads facade
remains registered. Android 11.6.3 can restrict and order Google ads/VAST
providers per Clips presentation. iOS
11.6.1 supports presentation-scoped pre-roll but not provider order, and its
native GAM VAST pipeline attributes the configured ad unit in ad analytics.
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.
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.
Storyteller AdMob SDK#
Add the AdMob package alongside the core SDK:
dependencies:
storyteller_sdk: ^11.6.5
storyteller_admob_sdk: ^11.6.5
The plugin requires Android API 24 or later and iOS 15.0 or later. Add your
Google Mobile Ads application ID to the Android application manifest and iOS
Info.plist as required by Google Mobile Ads.
Register AdMob before calling Storyteller.initialize, so the native module is
available before content can request an ad:
import 'package:storyteller_admob_sdk/storyteller_admob_sdk.dart';
import 'package:storyteller_sdk/storyteller_sdk.dart';
Future<void> configureStoryteller() async {
await StorytellerAdMobModule.setup(
StorytellerAdMobModuleConfiguration(
nativeAdUnit: (StorytellerAdRequestInfo request) {
if (request is StoriesAdRequestInfo) {
return 'YOUR_STORIES_NATIVE_AD_UNIT';
}
return 'YOUR_CLIPS_NATIVE_AD_UNIT';
},
bannerAdUnit: (request) => 'YOUR_FULLSCREEN_BANNER_AD_UNIT',
bottomBannerAdUnit: (request) => 'YOUR_CLIPS_BOTTOM_BANNER_AD_UNIT',
customKvps: () => {'app_section': 'storyteller'},
enableBannerAdPriority: false,
),
);
await Storyteller.initialize('YOUR_API_KEY');
}
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.
Ad-unit and KVP callbacks may return immediately or asynchronously. Flutter
resolves one bounded configuration snapshot for each native request. A missing,
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.
The standalone example under
packages/storyteller_admob_sdk/example/ uses Google's official platform test
ad units and intentionally excludes the GAM package so it remains a focused
standalone example. 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.
Storyteller GAM SDK#
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.
Basic Setup#
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.
dependencies:
storyteller_sdk: ^11.6.5
storyteller_gam_sdk: ^11.6.5
Then install the dependencies:
flutter pub get
Configure the GAM module#
Call StorytellerGAMModule.setup before Storyteller.initialize. Provide a
main ad unit for every request. All other values are optional.
import 'package:storyteller_gam_sdk/storyteller_gam_sdk.dart';
Future<void> configureGam() async {
await StorytellerGAMModule.setup(
StorytellerGAMModuleConfiguration(
adUnit: (StorytellerAdRequestInfo request) async {
if (request is StoriesAdRequestInfo) {
return '/YOUR_STORIES_AD_UNIT_ID';
}
if (request is ClipsAdRequestInfo) {
return '/YOUR_CLIPS_AD_UNIT_ID';
}
return 'YOUR_DEFAULT_AD_UNIT_ID';
},
bottomBannerAdUnit: (_) async =>
'/YOUR_CLIPS_BOTTOM_BANNER_AD_UNIT_ID',
customNativeTemplateIds: const StorytellerCustomNativeTemplateIds(
stories: 'YOUR_STORIES_TEMPLATE_ID',
clips: 'YOUR_CLIPS_TEMPLATE_ID',
),
customKvps: () => {
'appmode': 'prod',
},
publisherProvidedId: () {
return consentAllowsAdTracking ? currentPublisherProvidedId : null;
},
enableDebugLogging: true,
),
);
await Storyteller.initialize('YOUR_API_KEY');
}
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.
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 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.
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.
Native request enrichment#
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:
import com.storyteller.gam.sdk.flutter.StorytellerGAMModulePlugin
StorytellerGAMModulePlugin.registerAdRequestEnricher {
requestInfo, builder, completion ->
bidder.load(requestInfo) { targeting ->
targeting?.let { builder.addCustomTargeting("bidder_key", it) }
completion.onSuccess(builder)
}
}
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:
import storyteller_gam_sdk
StorytellerGAMModulePlugin.registerAdRequestConfiguration { requestInfo, request in
let targeting = await bidder.targeting(for: requestInfo)
var customTargeting = request.customTargeting ?? [:]
customTargeting["bidder_key"] = targeting
request.customTargeting = customTargeting
}
Clear it with
StorytellerGAMModulePlugin.clearAdRequestConfiguration().
Understanding ad request payloads#
Storyteller surfaces detailed context for every ad opportunity:
StoriesAdRequestInfoplacement– native placement code configured in Storyteller Studio.categories– list of category IDs associated with the story.story–ItemInfodescribing the story and its categories.adIndex– one-based index of the requested ad slot.ClipsAdRequestInfocollection– collection ID currently being viewed.clip–ItemInfofor the active clip.nextClip– optionalItemInfofor the clip that will follow.adIndex– one-based index of the requested ad slot.
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.
Logging and troubleshooting#
Set enableDebugLogging to true (the default) while integrating on Android;
switch it off for production if the extra module logging is not required. A
failed callback produces a value-free diagnostic and skips only the affected ad
opportunity.
The Showcase app centralises GAM setup inside
StorytellerService._configureGamModule.