Skip to main content

In-App Messaging (Flutter SDK)

This guide covers OpenCDP native in-app messages: content authored in the dashboard, delivered through the Data Gateway (/v1/in-app/messages/...), and rendered in your Flutter UI.

Not Customer.io in-app

If you use Customer.io dual-write (sendToCustomerIo: true), CustomerIOConfig.inAppConfig configures Customer.io's own in-app product. That is separate from OpenCDP native in-app. OpenCDP in-app is controlled only by OpenCDPConfig.enableInAppMessages (and related flags below).

For product concepts (quiet hours, page rules, analytics), see the platform guide: In-app messaging.


Package and source layout​

ItemLocation
pub.dev packageopen_cdp_flutter_sdk
Repositoryopencdp-flutter-sdk on GitHub

Important paths inside the package:

PathRole
lib/open_cdp_flutter_sdk.dartPublic OpenCDPSDK API (initialize, identify, syncInAppMessages, tracking helpers, inApp getter)
lib/src/models/config.dartOpenCDPConfig — enableInAppMessages, polling / realtime tuning
lib/src/in_app/in_app_manager.dartCDPInAppManager — sync, SSE, messageStream, setCurrentScreen, impression / click / dismiss
lib/src/in_app/in_app_realtime_client.dartSSE client for /v1/in-app/messages/stream
lib/src/implementation/sdk_implementation.dartHTTP calls to /sync, /impression, /click, /dismiss
lib/src/constants/endpoints.dartDefault Data Gateway base URL and path constants
lib/src/in_app/open_cdp_in_app_host.dartOpenCDPInAppHost — recommended auto-present entry for modal/banner + slot registry
lib/src/in_app/open_cdp_in_app_slots.dartOpenCDPInAppInlineSlot / OpenCDPInAppInboxSlot — place in your layout tree
example/lib/in_app/Older reference UI (prefer package OpenCDPInAppHost)

Prerequisites​

  1. Workspace API key (cdpApiKey) with access to the Data Gateway (same key as identify / track).
  2. Identify users with a stable identifier that matches People in your workspace before expecting deliveries.
  3. Workspace setting in-app enabled, and message content created in Broadcasts or Campaigns.

1. Enable native in-app in config​

Set enableInAppMessages: true. For the recommended path also set enableInAppAutoPresent: true and wrap your app with OpenCDPInAppHost so modal / banner show on Include screens without a custom listener.

await OpenCDPSDK.initialize(
config: OpenCDPConfig(
cdpApiKey: 'YOUR_CDP_API_KEY',
debug: kDebugMode,

// Native OpenCDP in-app (Data Gateway)
enableInAppMessages: true,
enableInAppAutoPresent: true,
// Prefer SSE push + catch-up sync; falls back to polling if stream is down
enableInAppRealtime: true,
inAppPollInterval: const Duration(seconds: 30),
inAppRealtimeStaleTimeout: const Duration(seconds: 60),
inAppRealtimeMaxBackoff: const Duration(seconds: 30),
inAppSyncLimit: 10,

autoTrackScreens: true, // recommended: feeds screen names into in-app page rules
),
);

// Entry point — present modal/banner on whatever screen synced
MaterialApp(
navigatorObservers: [
if (OpenCDPSDK.instance.screenTracker != null)
OpenCDPSDK.instance.screenTracker!,
],
builder: (context, child) => OpenCDPInAppHost(
child: child!,
// Optional branded modal/banner (auto-present stays on)
// modalBuilder: ...,
),
home: const HomePage(),
);

// In a screen — place the slot where the inline card should appear:
OpenCDPInAppInlineSlot(
slotId: 'home_above_balance', // optional multi-slot targeting
// builder: (context, message, {required onCta, required onDismiss}) =>
// MyInlineCard(message: message, onTap: onCta, onClose: onDismiss),
);

await OpenCDPSDK.instance.identify(
identifier: 'user_123',
properties: {'email': '[email protected]'},
);

Cross-platform overview: Mobile in-app messaging.


Inline / inbox slots (layout-bound)​

modal / banner use overlays. inline and inbox_card need a place in your widget tree — put an explicit slot where you want the card (do not use GlobalKeys to “hunt” a widget):

// Above a balance widget, for example:
Column(
children: [
OpenCDPInAppInlineSlot(slotId: 'home_above_balance'),
BalanceHeader(...),
],
)

// Or append inbox cards in a feed:
OpenCDPInAppInboxSlot()

Requires an ancestor OpenCDPInAppHost. Slots track impression / click / dismiss. Optional builder keeps your branded UI while auto-delivery stays on. Optional slotId reserves a placement when the host publishes with a matching targetSlotId (otherwise every slot can show the current inline — use one per screen).

Callbacks onInlineMessage / onInboxMessage remain available if you prefer fully custom state.


Custom UI (optional)​

If you disable auto-present, listen to messageStream and render yourself:

Config reference (native in-app)​

FieldDefaultMeaning
enableInAppMessagesfalseWhen true, creates CDPInAppManager, opens SSE (if enabled), and runs sync lifecycle.
enableInAppRealtimetrueWhen true and in-app enabled, connects to GET .../stream and triggers /sync on sync SSE events.
inAppPollInterval30sPoll interval when realtime is off or while SSE is disconnected.
inAppRealtimeStaleTimeout60sIf no bytes on the stream for this long, reconnect (backend heartbeats ~20s).
inAppRealtimeMaxBackoff30sCap for SSE reconnect backoff.
inAppSyncLimit10limit query param on /sync (1–50).
inAppPlatformOverridenullOptional platform query value (else ios / android / web).
inAppAppVersionOverridenullOptional app_version on sync and impressions.

The manager is always constructed after initialize so you can call OpenCDPSDK.instance.syncInAppMessages manually even if enableInAppMessages is false; automatic polling / SSE only run when enableInAppMessages is true.


2. Identity and realtime​

After identify, the SDK calls CDPInAppManager.setActiveIdentity internally so:

  • The SSE URL uses the correct person_id.
  • Local delivery state resets on user switch.

Call identify as soon as you know the user id (login, token refresh, etc.).


3. Screen names (page rules)​

Backend include / exclude screens filter on the screen query parameter of /sync.

Recommended: OpenCDPConfig.autoTrackScreens: true and register OpenCDPSDK.instance.screenTracker as a NavigatorObserver. The SDK wires onScreenChange → inApp.setCurrentScreen, so each navigation triggers a sync when the screen changes.

Manual: call await OpenCDPSDK.instance.inApp?.setCurrentScreen('checkout'); whenever your route changes (same strings as in the dashboard editor).

The example app exposes bottom tabs (home, cart, profile, inbox) for targeting tests.


4. Receiving messages: CDPInAppManager​

Access the manager after initialization:

final manager = OpenCDPSDK.instance.inApp;
if (manager == null) {
// SDK not initialized
return;
}

messageStream​

Eligible messages are emitted on messageStream as InAppMessage instances. The SDK:

  • Calls /sync on a timer (when realtime is off or disconnected) or after SSE sync events.
  • Applies client-side filters (expiry, dismissals, persistence caps). It does not re-sort: order matches the backend contract (priority desc, then queue order).

Subscribe once near the root of your app (see SDK example/lib/in_app/in_app_host.dart for a full pattern):

late final StreamSubscription<InAppMessage> _inAppSub;

@override
void initState() {
super.initState();
_inAppSub = OpenCDPSDK.instance.inApp!.messageStream.listen((message) async {
switch (message.renderType) {
case InAppRenderType.modal:
// showDialog(...)
break;
case InAppRenderType.banner:
// overlay at top of screen
break;
case InAppRenderType.inline:
case InAppRenderType.inboxCard:
// add to inline list / inbox
break;
case InAppRenderType.unknown:
break;
}
await OpenCDPSDK.instance.inApp?.trackImpression(message);
});
}

@override
void dispose() {
_inAppSub.cancel();
super.dispose();
}

Or wrap your scaffold with InAppHost and handle inline/inbox deliveries via onInlineMessage.

Render types​

Each message includes a render_type from the backend. Branch on message.renderType and render the matching UI pattern:

Backend valueInAppRenderTypeUI pattern
modalmodalFull-screen dialog
bannerbannerTop overlay strip
inlineinlineEmbedded in the current page
inbox_cardinboxCardCard in an inbox or list
InAppRenderTypeYour responsibility
modale.g. showDialog
bannere.g. Overlay at top
inline / inboxCardRender inside your scaffold; you track impression / click / dismiss when appropriate

5. Tracking (required for analytics)​

Use CDPInAppManager methods so screen and platform stay consistent with the manager:

CallWhen
trackImpression(message)When the message is visible on screen
trackClick(message: message, actionId: cta.id)When the user taps a CTA (actionId must match InAppCta.id)
trackDismiss(message: message, reason: ...)When the user closes without a tracked CTA

InAppDismissReason maps to backend reason strings (user_close, app_backgrounded, etc.).

Use SDK methods only — do not call Data Gateway interaction URLs directly.

The example host calls trackImpression right after opening a modal/banner, then trackClick or trackDismiss when the dialog completes.


6. Manual sync (advanced)​

You can bypass the stream and fetch on demand:

final messages = await OpenCDPSDK.instance.syncInAppMessages(
screen: 'inbox',
platform: 'android',
appVersion: '1.0.0',
limit: 20,
tzOffsetMinutes: DateTime.now().timeZoneOffset.inMinutes,
);

Manual sync returns a list; it does not push to messageStream. Route each returned message through the same render-type switch as the stream listener.

The manager already passes DateTime.now().timeZoneOffset.inMinutes on automatic syncs so quiet hours with timezone_mode: user_local work without extra plugins. If you call syncInAppMessages yourself, pass the same when your message definitions use user-local quiet hours.


7. HTTP surface​

Unless cdpEndpoint is set, the SDK uses the Data Gateway base URL from CDPEndpoints (see endpoints.dart in the package). Copy your workspace gateway URL from Settings if you override the default.

  • GET /v1/in-app/messages/sync
  • GET /v1/in-app/messages/stream
  • POST /v1/in-app/messages/{deliveryId}/impression|click|dismiss

All requests use the CDP API key in the same way as other Data Gateway routes.


8. Customer.io dual-write (optional)​

When sendToCustomerIo: true, you may pass CustomerIOConfig.inAppConfig for Customer.io's SDK features. That does not replace or configure OpenCDP native in-app. Keep enableInAppMessages: true for OpenCDP deliveries regardless of Customer.io.


Test with the example app​

  1. Run the Flutter example app.
  2. Enter API key, gateway URL (YOUR_DATA_GATEWAY_URL from workspace Settings), and test person ID.
  3. Switch tabs to change logical screen.
  4. Tap Sync now or wait for automatic delivery.
  5. Confirm modal, banner, inline, or inbox_card renders and metrics fire.