MySuperWallDocs

Flutter SDK · v1.1.0 · 0 ms Offline Cache

Ship paywalls without shipping an app update.

You design the paywall and decide when it appears in the MySuperWall dashboard. The SDK fetches it by trigger key and presents it in your app. Purchases stay with your own in-app purchase code.

1Create an app and a paywall in the dashboard
2Create a trigger key and attach the paywall
3Call handleTrigger in your app
How purchases work. MySuperWall does not process payments. The paywall tells your app which product was tapped (onPurchase); you run the purchase with RevenueCat, in_app_purchase or any other library.

Quick start

From a new account to your first paywall in your app. Each step is done in the dashboard unless it says otherwise.

  1. Create an account and an appSign up, verify your email, then open Apps → New app. Enter a name, the platform and your bundle ID (iOS) or package name (Android). You land on the setup checklist, which tracks your progress.
  2. Add your productsOpen Products → New product and enter each subscription or purchase. The product ID must match your App Store or Google Play ID exactly. If you connected a store account, you can sync products instead.
  3. Create a paywallOpen Paywalls → New paywall, pick a template and edit the texts. On the Products tab, link your products to the template's price slots. Turn the paywall on when it is ready.
  4. Add a triggerOpen Triggers → New trigger. Choose a key such as onboarding_finish and the paywall to show. Keep it simple at first; A/B tests and targeting are under Advanced options.
  5. Connect the SDKIn your Flutter app, add the package, initialize it with your app key and call handleTrigger with the same trigger key. The code is below, with your real key on the app's Overview page.
  6. Check it workedRun the app. The Overview page shows when the SDK has connected and when your first paywall was delivered. If nothing shows, see Troubleshooting.
// pubspec.yaml
dependencies:
  mysuperwall_sdk: ^1.3.1

// main.dart
await MySuperWallClient.initialize(appKey: 'YOUR_APP_KEY');

// where the paywall should appear
await MySuperWallClient.instance.handleTrigger(
  context,
  triggerKey: 'premium',
  onPurchase: (productId) async { /* run your purchase here */ },
);
Good to know. Edits to a paywall, product list or trigger reach your app the next time it fetches them. You do not need to ship a new build. Paywalls are cached on the device, so use clearCachedPaywalls() while testing.

Install the SDK

Requires Flutter 3.35+ and Dart 3.9+.

Open guided setup · pub.dev

flutter pub add mysuperwall_sdk:^1.3.1

Initialize

Call initialize once, before runApp. Your app key is shown on the app's Overview page (SDK key) and in Settings.

import 'package:mysuperwall_sdk/mysuperwall_sdk.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await MySuperWallClient.initialize(
    appKey: 'YOUR_APP_KEY',
  );

  runApp(const MyApp());
}
ParameterDefaultDescription
appKeynoneIdentifies your app. The SDK also sends your bundle ID / package name, so the app can be matched by those too.
userIdautoYour own user ID. If omitted, an anonymous UUID is generated and stored on the device.
requestTimeout8 sTimeout for backend requests.
failOpenOnNetworkErrortrueControls whether initialization may continue when validation is unavailable. It does not authorize an offline paywall. Catch presentation request errors; cached content alone cannot authorize display.

Afterwards use MySuperWallClient.instance anywhere. MySuperWallClient.isInitialized tells you whether setup completed.

Load store prices and prepare the paywall

After initialization, fetch real products from your existing billing provider and build localizedProducts keyed by exact product ID. Values must include displayPrice, priceString and currencyCode; include derived period prices when used. The snippet shows the SDK call, not a complete billing adapter. Wait for successful price and paywall loading before enabling the entry point. Currency follows the store account; it is not converted from device language. Do this once at startup and again only when prices or the language change: new prices or a new locale before every presentation discard the prepared paywall and make each opening slow.

await MySuperWallClient.instance.setProductData(localizedProducts);
await MySuperWallClient.instance.setPreparedPaywallPreferences(
  preferredTriggerKeys: ['YOUR_TRIGGER_KEY'], warmPool: false,
);
await MySuperWallClient.instance.prefetchAllActivePaywalls();
await MySuperWallClient.instance.warmPreparedPaywalls();

Add the host widget

Wrap your app in MySuperWallHost. It keeps paywalls pre-loaded in the background so they appear instantly. It is recommended; without it the SDK falls back to a normal route presentation.

MaterialApp(
  builder: (context, child) => MySuperWallHost(child: child!),
  home: const HomePage(),
);

Show a paywall

Call handleTrigger at the moment you want a paywall. The trigger key decides which paywall (or A/B variant) the user gets, based on your dashboard configuration.

Purchase and restore callbacks do not automatically show loading. Mount MySuperWallHost, then use this helper in your controller/service for both prepared and normal paywalls. Start loading before product lookup or billing; always clear it in finally and prevent repeated taps. Without the host, use your app's loading overlay above the paywall.

// Keep this helper and billing handlers in your existing controller/service.
// Mount MySuperWallHost in the app builder before presenting paywalls.
bool isStoreActionInProgress = false;
Future<void> runWithPurchaseLoading(Future<void> Function() action) async {
  if (isStoreActionInProgress) return;
  isStoreActionInProgress = true;
  MySuperWallClient.setHostLoadingOverlayVisible(true);
  try {
    await action();
  } finally {
    MySuperWallClient.setHostLoadingOverlayVisible(false);
    isStoreActionInProgress = false;
  }
}

purchaseProduct and restorePurchases below are your existing asynchronous billing methods, not SDK APIs. Await the actual purchase or restore, handle cancellation/errors, update entitlements, and report only confirmed outcomes inside them. Use the same wrappers for presentPreparedPaywallIfAvailable callbacks.

final result = await MySuperWallClient.instance.handleTrigger(
  context,
  triggerKey: 'onboarding_finish',
  onPurchase: (productId) async {
    await runWithPurchaseLoading(() => purchaseProduct(productId));
  },
  onRestore: () async {
    await runWithPurchaseLoading(restorePurchases);
  },
  onDismiss: () {},
);

if (!result.show) {
  // No paywall for this user/trigger (disabled, holdout, offline...).
  // Let the user continue.
}

PaywallResult fields: show, paywall, reason, message, statusCode, details. Always handle show == false; it is a normal outcome (for example holdout groups or audience rules).

CallbackWhen
onPurchase(productId)User taps a purchase button. The SDK records purchase_started automatically.
onRestoreUser taps "Restore".
onDismissPaywall closed. The SDK records the dismiss event automatically.
onOpenTerms / onOpenPrivacy / onOpenUrlUser taps a legal or external link. Handle these callbacks to open the links in your app.

Pass metadata: {...} to send extra context used for audience rules.

Handle purchases

The SDK records purchase_started and dismiss events itself. Report the outcome so your dashboard analytics show conversions and revenue:

Adapt your billing service to return a typed outcome: purchased, cancelled, pending or failed. A boolean false does not distinguish cancellation from error. Keep this adapter in your existing controller/service.

// Application adapter contract, not an SDK-provided billing service.
enum BillingOutcome { purchased, cancelled, pending, failed }

// Inside onPurchase, within runWithPurchaseLoading:
final client = MySuperWallClient.instance;
final outcome = await myPurchaseService.buy(productId); // BillingOutcome
switch (outcome) {
  case BillingOutcome.purchased:
    client.setSubscriptionStatus(true); // real entitlement confirmed
    await client.trackPurchaseSuccess(productId: productId);
    client.dismiss();
    break;
  case BillingOutcome.cancelled:
    await client.trackPurchaseCancelled(productId: productId);
    break;
  case BillingOutcome.failed:
    await client.trackPurchaseFailed(productId: productId);
    break;
  case BillingOutcome.pending:
    break; // wait for the provider's actual terminal result
}

Follow-ups reuse your purchase callbacks. Omit optional paywallId and triggerKey overrides unless they identify the currently displayed paywall. The SDK records purchase context when a product button is tapped. Restore success must identify the current paywall. Report revenueUsd only when the actual USD amount is known.

dismiss() closes the visible paywall programmatically.

Product prices

Paywall templates show prices such as {{weeklyPriceText}}. The SDK has no built-in store connection, so pass the localized prices you already read from the store, keyed by product ID:

await MySuperWallClient.instance.setProductPrices({
  'weekly_sku':  '\$6.99',
  'monthly_sku': '\$19.99',
});

For intro offers, per-day/week/month text and eligibility, use structured data:

await MySuperWallClient.instance.setStructuredProductData({
  'weekly_sku': const MySuperWallProductDataEntry(
    displayPrice: '\$6.99',
    pricePerWeekText: '\$6.99',
    introDisplayPrice: '\$1.99',
    introEligible: true,
    introUsed: false,
  ),
});
  • If the user is intro-eligible, the price placeholders show the intro price; otherwise the regular price.
  • Call this before showing a paywall. Pass clearCachedPaywalls: true if prices change while paywalls are already cached.
  • Product IDs must match the ones configured in the dashboard under Products.

Prefetching

Fetch paywalls ahead of time so they open without a spinner:

await MySuperWallClient.instance.prefetchPaywall(triggerKey: 'onboarding_finish');

// or everything that is active:
await MySuperWallClient.instance.prefetchAllActivePaywalls();

Paywalls are cached on disk. clearCachedPaywalls() removes them. With MySuperWallHost, one ready paywall is kept warm by default (Flutter SDK 1.2.0 defaults to none: call configurePreparedPaywallPool(maxCount: 1) after initialize); tune it with configurePreparedPaywallPool(maxCount: n) and setPreparedPaywallPreferences(preferredTriggerKeys: [...]).

Lifecycle triggers

Published Flutter 1.3.1 computes these fixed lifecycle keys during initialization. Call handleLifecycleTriggers after UI readiness; expanded automatic behavior below requires updated unpublished source.

KeyFires
mysuperwall.installFirst SDK use on this device
mysuperwall.updateFirst launch after an app version change
mysuperwall.sessionEvery session
mysuperwall.session_3 / _5 / _10 / _20 / _50Once, at that session count
mysuperwall.day_1 / _3 / _7 / _14 / _30Once, that many days after first SDK use
@override
void initState() {
  super.initState();
  WidgetsBinding.instance.addPostFrameCallback((_) {
    MySuperWallClient.instance.handleLifecycleTriggers(context,
      onPurchase: (id) => buy(id));
  });
}

It shows at most one paywall per call (the first match). prefetchLifecycleTriggers() preloads them.

Updated source only — not in pub.dev 1.3.1. The automatic host runner, readiness control, custom thresholds, foreground event and 30-day history/queue are still unpublished. Inspect the installed SDK source before using the APIs below.
KeyFires
mysuperwall.foregroundEach return from the background
mysuperwall.session_NReach N total sessions, once
mysuperwall.day_NN days since first SDK use, once
mysuperwall.active_minutes_NN total active minutes, once; background time excluded
// Updated source only. Register existing application-service handlers once.
client.setPresentationHandlers(
  onPurchase: purchaseThroughExistingBilling,
  onRestore: restoreThroughExistingBilling,
);

// Preserve your existing MaterialApp/GetMaterialApp builder.
MySuperWallHost(
  lifecycleReady: onboardingComplete,
  child: existingBuilderResult,
)

In the updated source, sessions start at SDK initialization or a return after at least 30 minutes in the background. Days start at first SDK use, not store installation. One-time signals expire 30 days after occurrence; history before collection began cannot be recovered. Automatic display waits for readiness and valid billing handlers. The host does not require purchase callbacks.

Read the complete lifecycle and recovery integration contract

Users & locale

MySuperWallClient.instance.setUserId('user_123'); // after login
MySuperWallClient.instance.setLocale('de');       // paywall language

Changing either value resets cached paywalls so the next one is fetched for the new identity. A/B variants are sticky per user, so use a stable userId for consistent experiments. Set the language once at startup and when the user changes it, never before each presentation.

Automation actions

Configure follow-up triggers on the paywall’s Follow-up tab. The default runner handles supported rules; do not install a custom handler just to enable recovery.

  • Publish the source paywall and activate the target trigger and published paywall in the same app/platform. The built-in runner applies delay, once-per-session, cooldown, chain depth, premium and repeat protection.
  • setAutomationActionHandler replaces the default runner. Use it only for an intentional custom runner that preserves every policy and billing callback; a delay-only handler is incomplete.
  • The 2026-11-01 cancellation forwarding and prepared-presentation follow-up fixes are in updated source only, not published 1.3.1. Record the actual billing result and inspect your installed SDK before claiming recovery works.

Push notifications (optional)

Send notifications from the dashboard through your own Firebase project, for example a reminder after someone closes the paywall that opens a discount paywall. The SDK has no Firebase dependency: your app reads the FCM token and hands it over.

Requires Flutter SDK 1.3.0 or later.

  1. Connect Firebase. In the dashboard open Integrations → Push notifications and upload a Firebase service account key (role: Firebase Cloud Messaging Admin).
  2. Register the device. After the user allows notifications, pass the FCM token to the SDK, and again whenever it refreshes. It is registered for the current user and language, and again by itself when you change either.
  3. Handle taps. Give the notification data to the SDK when the user taps it. It reports the tap and returns the trigger to open, so a discount trigger shows your offer.
  4. Create a notification. In the dashboard open Notifications, choose when to send (for example after the paywall was closed), write the text per language and pick the trigger each reminder opens.
final messaging = FirebaseMessaging.instance;
await messaging.requestPermission();
final token = await messaging.getToken();
if (token != null) await MySuperWallClient.instance.setPushToken(token);
messaging.onTokenRefresh.listen(MySuperWallClient.instance.setPushToken);

// Taps: app in the background, and app started by the notification.
Future<void> openFrom(RemoteMessage message) async {
  final key = await MySuperWallClient.instance
      .trackNotificationOpened(message.data);
  if (key == null || !context.mounted) return;
  await MySuperWallClient.instance.prepareTrigger(key);
  if (context.mounted) {
    await MySuperWallClient.instance.handleTrigger(context, triggerKey: key);
  }
}
FirebaseMessaging.onMessageOpenedApp.listen(openFrom);
final initial = await messaging.getInitialMessage();
if (initial != null) await openFrom(initial);

// Stop notifications (logout, or the user turned them off):
await MySuperWallClient.instance.clearPushToken();

Each device gets the text for its language (the SDK locale); devices without a matching language get the default one. Someone who just bought is skipped, and a purchase cancels pending notifications. Translating the text with DeepL is a Premium feature.

A notification's trigger is usually not one of your prepared paywalls. Wait until startup (prices and language) has finished and call prepareTrigger before presenting, otherwise the paywall can open as a blank page.

Android shows FCM notifications only while the app is in the background. Keep the WAKE_LOCK permission in your manifest (Firebase needs it) and request POST_NOTIFICATIONS on Android 13+.

iOS: enable the Push Notifications capability and the Remote notifications background mode, and upload your APNs key in the Firebase console (Project settings → Cloud Messaging).

Offline & errors

  • Paywalls are cached on disk; a cached paywall opens without network. Stale cache is used only on transient failures, for at most 24 hours.
  • With failOpenOnNetworkError: true (default), a network failure returns PaywallResult(show: false, reason: 'network_unavailable').
  • Configuration problems throw MySuperWallException with message, code, statusCode and details. Wrap calls in try/catch.
  • healthCheck() returns true if the backend is reachable.

API reference

MethodPurpose
MySuperWallClient.initialize({appKey, userId, httpClient, requestTimeout, failOpenOnNetworkError})Set up the SDK (static, once)
handleTrigger(context, {triggerKey, metadata, onPurchase, onRestore, onDismiss, onOpenUrl, onOpenTerms, onOpenPrivacy})Resolve and show a paywall
handleLifecycleTriggers(context, {...})Show the first matching lifecycle paywall
prefetchPaywall({triggerKey, metadata})Fetch and cache one paywall
prefetchAllActivePaywalls()Fetch and cache all active paywalls
setProductPrices(map) / setProductData(map) / setStructuredProductData(map)Provide store prices
trackPurchase / trackPurchaseSuccess / trackPurchaseFailed / trackPurchaseCancelledReport purchase events
trackDismiss({paywallId, ...})Report a dismiss (done automatically by handleTrigger)
setUserId / setLocaleChange identity or language
setAutomationActionHandler(handler)Run dashboard-defined follow-up actions
dismiss()Close the visible paywall
clearCachedPaywalls()Remove disk cache
healthCheck()Check backend connectivity
setPushToken(token)Register the FCM token for this device
clearPushToken()Stop notifications for this device
trackNotificationOpened(data)Report a notification tap and get the trigger to open
handleNotificationOpened(context, data, {...})Report the tap and show the notification's paywall

AI agents

Connect Claude Code, Codex or Cursor to your account and let it build paywall drafts for you. The agent works through the MySuperWall MCP server. You review every draft and decide what goes live.

Drafts only. Everything an agent creates is inactive. It cannot activate a paywall or trigger, edit one that is live, or delete anything.

Live editor pairing (recommended)

Open a paywall in the editor and click AI agent. Add the editor MCP to your agent once, then paste the prompt with the pairing code shown in the editor. The agent edits the paywall in front of you: it picks or designs the template, writes the copy, links products, adds languages and checks its work with screenshots. No key is needed; the one-time code connects the agent to that editor tab only, and you can disconnect it at any time.

claude mcp add --transport http mysuperwall-editor https://mysuperwall.com/editor-mcp
# then, in Claude Code:
Connect to the mysuperwall-editor MCP with pairing code: ABCD-EFGH-JKLM

Connect an agent

  1. Create a keyOpen the account menu, choose AI agents and select New key. Pick the apps the agent may use and how long the key lasts. The key is shown once.
  2. Connect your agentThe page shows a ready command for Claude Code, Codex and Cursor. For Claude Code it looks like the command below.
  3. Ask for a paywallDescribe what you want. The agent saves drafts and gives you a preview link. Review them under Paywalls and activate the ones you like.
claude mcp add --transport http mysuperwall https://mysuperwall.com/mcp \
  --header "Authorization: Bearer msw_pat_YOUR_KEY"

Example requests

  • List my MySuperWall apps and show me what paywalls the first one has.
  • Design a dark paywall from scratch for my fitness app with a weekly and a yearly plan, check how it looks, and fix anything that is clipped.
  • Duplicate my live paywall, rewrite the headline to focus on saving time and add a Turkish version.

What the agent can do

ToolWhat it does
list_apps, get_appRead your apps with their products, paywalls and triggers.
list_templates, get_template, get_template_guideRead built-in and custom templates, and the rules for writing a new one.
create_template, update_templateSave a template designed from scratch (HTML). Rejected with a reason if it breaks a rule.
create_paywall, update_paywall, duplicate_paywallCreate or edit draft paywalls: content, products, languages.
create_product, create_triggerRegister a store product, or add a draft trigger for a paywall.
request_image_uploadReturns a one-time command to upload a local image (PNG, JPEG, WebP or GIF, up to 5 MB).
preview_template, preview_paywallRender a design to an image so the agent can check it. Uses sample prices. Available when screenshots are enabled on the server.

Safety and limits

  • A key only reaches the apps you chose, and it expires. Revoke it any time on the AI agents page.
  • Agents cannot reach store credentials, billing, team members, SDK keys or your account.
  • Templates are checked on save. Network calls, external scripts, redirects and iframes are not allowed. Template HTML has a 150 KB safety size check.
  • Short-lived request throttling protects the service from abusive bursts.
  • Every agent action appears in the activity log as agent:key name.

Tips

Paste the short guide at /docs/agents.md into your project's CLAUDE.md or AGENTS.md so the agent knows how to work with MySuperWall. Ask it to preview after every significant change.

If something fails

401 Unauthorized
The key was revoked, has expired or was copied incompletely. Create a new one.
"This paywall is live"
Agents cannot edit live paywalls. Ask it to duplicate the paywall and edit the copy.
"Screenshots are not enabled"
The server has no renderer configured. The agent can still give you the preview link.
A template is rejected
The message says which rule it broke. The agent can read get_template_guide for the full list.

Live SDK Debugger & Event Stream Console

Inspect real-time paywall trigger evaluations, audience targeting decisions, and conversion events as they occur on physical test devices.

1Open Live Debugger in your Dashboard navigation
2Run your app on simulator or physical device
3Watch live events stream in real-time via Server-Sent Events (SSE)

The console allows you to:

  • Filter Event Types: Switch between Triggers, Paywall Presentations, Purchases, Holdout allocations, and Diagnostic Warnings.
  • Inspect Audience Rule Traces: See why a paywall was shown or skipped, which rule conditions passed, and the assigned A/B test variant.
  • Interactive Trigger Simulator: Simulate trigger keys with custom user attributes directly in the dashboard before testing in the app.

Troubleshooting

Nothing shows, result.show is false
Check result.reason. Common causes: the trigger key does not exist or has no paywall attached, the paywall is not published, the user is in a holdout group or fails an audience rule, or the device is offline.
"Prepared controller missing" in logs
Harmless, but add MySuperWallHost above your Navigator so paywalls can be pre-built.
unmounted_context error
The widget was disposed while the paywall was loading. Check mounted before calling, or trigger from a stable parent.
Prices show as placeholders
Call setProductPrices / setStructuredProductData before showing the paywall, and make sure product IDs match the dashboard.
Old design still appears after editing
Paywalls are cached; the update arrives on the next fetch. Call clearCachedPaywalls() while testing.
Different paywall for the same user
Pass a stable userId. Without one, a new install gets a new anonymous ID and can land in a different A/B variant.