Skip to content

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.

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.

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<StorytellerInitializeResult> configureClientAds({
  required StorytellerClientAdProvider provider,
}) async {
  await Storyteller.setClientAdProvider(
    provider,
    adSource: 'YOUR_AD_SOURCE',
    timeout: const Duration(seconds: 8),
  );

  return Storyteller.initialize('YOUR_API_KEY');
}

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:

StorytellerAd videoAdFromService({
  required String id,
  required String advertiserName,
  required String videoUrl,
  required String impressionUrl,
  required String completionUrl,
  required String destination,
  required String actionText,
  int durationSeconds = 0,
  String? playcardUrl,
}) {
  return StorytellerAd.video(
    id: id,
    advertiserName: advertiserName,
    videoUrl: videoUrl,
    durationSeconds: durationSeconds,
    playcardUrl: playcardUrl,
    trackingPixels: [
      StorytellerAdTrackingPixel(url: impressionUrl),
      StorytellerAdTrackingPixel(
        event: StorytellerAdTrackingEvent.videoComplete,
        url: completionUrl,
      ),
    ],
    action: StorytellerAdAction(
      type: StorytellerAdActionType.externalApp,
      urlOrStoreId: destination,
      text: actionText,
    ),
  );
}

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.

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 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:

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: ^12.2.0
  storyteller_vast_sdk: ^12.2.0

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<StorytellerInitializeResult> 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,
    ),
  );

  return 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. The default is pathSegment; set queryString explicitly for query endpoints, as in the example. enableDebugLogging defaults to false.

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',
    },
  ),
);

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.

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.

import 'package:flutter/foundation.dart';
import 'package:storyteller_vast_sdk/storyteller_vast_sdk.dart';

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. 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.

Storyteller AdMob SDK#

Add the AdMob package alongside the core SDK:

dependencies:
  storyteller_sdk: ^12.2.0
  storyteller_admob_sdk: ^12.2.0

The plugin requires Android API 24 or later and iOS 15.0 or later. Complete the Google Mobile Ads application setup below before running AdMob.

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<StorytellerInitializeResult> 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,
    ),
  );

  return 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. 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.

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: ^12.2.0
  storyteller_gam_sdk: ^12.2.0

Complete the Google Mobile Ads application setup below for GAM as well. 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_module.dart';
import 'package:storyteller_sdk/storyteller_sdk.dart';

Future<StorytellerInitializeResult> configureGam({
  required String? Function() consentGatedPpid,
}) 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_NETWORK/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'},
      requestCustomKvps: (StorytellerAdRequestInfo request) => {
        if (request is ClipsAdRequestInfo) 'collection': request.collection,
        if (request is StoriesAdRequestInfo)
          'categories': request.categories.join(','),
      },
      publisherProvidedId: consentGatedPpid,
      enableDebugLogging: true,
    ),
  );

  return 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. 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.

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#

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.

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. 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.

The Showcase app centralises GAM setup inside StorytellerService._configureGamModule.

Google Mobile Ads application setup#

For GAM or AdMob, add the Google Mobile Ads application ID for each platform. In android/app/src/main/AndroidManifest.xml, inside <application>:

<meta-data
    android:name="com.google.android.gms.ads.APPLICATION_ID"
    android:value="YOUR_GOOGLE_MOBILE_ADS_APPLICATION_ID" />

In ios/Runner/Info.plist:

<key>GADApplicationIdentifier</key>
<string>YOUR_GOOGLE_MOBILE_ADS_APPLICATION_ID</string>

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.