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.

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:

  • StoriesAdRequestInfo
  • placement – native placement code configured in Storyteller Studio.
  • categories – list of category IDs associated with the story.
  • story – ItemInfo describing the story and its categories.
  • adIndex – one-based index of the requested ad slot.
  • ClipsAdRequestInfo
  • collection – collection ID currently being viewed.
  • clip – ItemInfo for the active clip.
  • nextClip – optional ItemInfo for 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.