In-App Takeover setup

Integrate the EngageApp SDK (Web or Flutter) so full-screen in-app takeover campaigns render natively when your users open your app.

An In-App Takeover is a full-screen message rendered natively by our SDK when your end-user next opens your app. Delivery is pull-based: when a campaign targets a contact, the takeover is queued; the SDK fetches and renders it on the next app foreground, then reports impressions, clicks, and dismissals back to EngageApp automatically.

1. Activate the channel

Go to Channels → Add channel → In-App Takeover. This is a platform-managed channel, so there are no provider credentials. You can tune the display rules:

  • Default lifetime (hours): how long a takeover stays fetchable before it expires unshown.
  • Max per app open: how many takeovers the SDK shows per foreground.
  • Min gap between (seconds): minimum spacing between takeovers.

2. Create a publishable key

Under Settings → Developers → API keys, choose Publishable (pk_) and click Create key. Copy the pk_… value. It is shown only once. Publishable keys are safe to embed in your app and are sent by the SDK as the X-Publishable-Key header.

3. Author a takeover template

Go to Templates → New template → In-App Takeover. Set a headline, body, optional image or video, theme colors, and call-to-action buttons. Each button has an action: deeplink (route within your app), url (open a web URL), or dismiss. Use {{ first_name }} and other variables; they are resolved per contact at send time. Then send it from a campaign's send message step using your In-App channel.

To collect feedback in the takeover, turn on Ask a survey and pick a survey. Both SDKs show its questions under the copy and send the answers. See Surveys in campaigns.

YouTube videos and Shorts

Set the media block to YouTube and paste any YouTube link: a watch?v= URL, a youtu.be short link, a /shorts/ URL, or an /embed/ URL. The video id is extracted for you and the takeover embeds the player directly, so there's no file to host. A /shorts/ link is marked vertical automatically, which switches the embed to 9:16; you can toggle that by hand if you paste a Short as a regular watch link.

Autoplay, loop and muted work the same as for a video file. YouTube blocks autoplay with sound, so an unmuted embed waits for a tap. The poster field is optional: with nothing set, YouTube's own thumbnail is used as the still frame. On Flutter, embeds need a webview (see step 5).

Full-screen video

Set the media block to Video and paste an MP4 (H.264) URL. Video defaults to Full screen: it fills the takeover edge-to-edge with your copy and buttons laid over it behind a readability scrim. Switch the placement to Banner at top to keep the classic hero layout instead. The same two placements work for images and YouTube embeds.

  • Poster image: the still frame shown before playback starts, and the fallback when video can't play. Required if you turn autoplay off.
  • Autoplay / Loop / Muted: all on by default. Browsers and mobile OSes block autoplay with sound, so an unmuted video waits for a tap; the Web SDK retries muted and then shows playback controls.

On Flutter, playback is opt-in so the SDK stays dependency-free: pass a videoBuilder to EngageInAppConfig (see step 5). Without one, video takeovers render the poster image.

4. Integrate the Web SDK

Install @engageapp/inapp-web:

npm install @engageapp/inapp-web

Initialise, register handlers, identify your user, and fetch on every app foreground:

import { EngageInApp } from '@engageapp/inapp-web';

// 1. Initialise with your PUBLISHABLE key (safe in client code).
EngageInApp.init({
  publishableKey: 'pk_your_key'
  // apiBaseUrl defaults to https://api.engageapp.xyz
});

// 2. Handle button taps (deep links / URLs) in your app.
EngageInApp.setHandlers({
  onButtonTap: (action) => {
    if (action.type === 'deeplink') router.go(action.value);
    if (action.type === 'url') window.location.assign(action.value);
  }
});

// 3. Identify the signed-in user by your own id.
await EngageInApp.identify('user-123', { firstName: 'Ada', identityHash: hashFromYourServer }); // required for new workspaces, see step 6

// 4. Fetch + render the next pending takeover. Call on every
//    app foreground / route into the app.
await EngageInApp.onAppForeground();

The SDK renders the takeover as a full-screen overlay and reports the impression, clicks, and dismissal for you. See the Web SDK reference for the full API.

5. Integrate the Flutter SDK

Add engageapp_inapp:

flutter pub add engageapp_inapp

Attach a navigator key so the SDK can present the takeover without a BuildContext, then initialise and identify:

final navigatorKey = GlobalKey<NavigatorState>();

void main() {
  EngageInApp.instance.init(EngageInAppConfig(
    publishableKey: 'pk_your_key',
    navigatorKey: navigatorKey,
  ));
  EngageInApp.instance.setHandlers(
    onButtonTap: (action, _) {
      if (action.type == 'deeplink') myRouter.go(action.value);
    },
  );
  runApp(MyApp(navigatorKey: navigatorKey));
}

// Attach the same key to your MaterialApp:
// MaterialApp(navigatorKey: navigatorKey, home: HomeScreen());

// After sign-in:
await EngageInApp.instance.identify('user-123', firstName: 'Ada', identityHash: hashFromYourServer); // required for new workspaces, see step 6

Call onAppForeground() whenever your app resumes, for example from a WidgetsBindingObserver:

@override
void didChangeAppLifecycleState(AppLifecycleState state) {
  if (state == AppLifecycleState.resumed) {
    EngageInApp.instance.onAppForeground();
  }
}

To play full-screen video takeovers, hand the SDK a player. It ships without a video dependency, so you choose the package (for example video_player) and the SDK renders whatever widget you return:

EngageInApp.instance.init(EngageInAppConfig(
  publishableKey: 'pk_your_key',
  navigatorKey: navigatorKey,
  videoBuilder: (context, media) => MyTakeoverVideo(
    url: media.url,
    autoplay: media.autoplay,
    loop: media.loop,
    muted: media.muted,
  ),
));

The widget is laid out to fill its slot, so size it to cover (e.g. wrap the player in a FittedBox with BoxFit.cover). The same builder handles YouTube: check media.isYouTube and load media.embedUrl in a webview (or hand media.videoId to a YouTube player package), otherwise play media.url directly.

Without a videoBuilder, playable takeovers render media.thumbnail (the author's poster, or YouTube's own thumbnail for an embed). Text, buttons, and reporting all still work.

Requires Flutter 3.27+. See the Flutter SDK reference for the full API.

6. Identity verification (required for new workspaces)

A publishable key authenticates your app, not the specific user. To stop a malicious client from passing another user's external_id, the SDK proves who the user is with an HMAC computed by your backend. New workspaces require this from day one: until it is set up, the SDK's identify, fetch and report calls are rejected with 401 and no takeover is shown.

  1. Go to Settings → Developers → In-app identity verification and click Generate secret. It is shown once; store it on your server (for example as ENGAGEAPP_INAPP_IDENTITY_SECRET). Never ship it in your app.
  2. On your server, compute HMAC-SHA256 of the user's external_id (UTF-8), keyed by the secret string exactly as shown (do not hex-decode it), and hex-encode the result (64 lowercase hex characters).
  3. Return that digest to your signed-in client and pass it to identify as identityHash. The SDK sends it as the X-Identity-Hash header on every call.
// Node.js server
import { createHmac } from 'node:crypto';

// The secret from Settings > Developers, stored server-side only.
// Use the secret string exactly as shown (do not hex-decode it).
export function identityHash(externalId: string): string {
  return createHmac('sha256', process.env.ENGAGEAPP_INAPP_IDENTITY_SECRET!)
    .update(externalId, 'utf8')
    .digest('hex');
}

// Go server
mac := hmac.New(sha256.New, []byte(secret)) // secret string as-is
mac.Write([]byte(externalID))
hash := hex.EncodeToString(mac.Sum(nil))

# Python server
import hmac, hashlib
hash = hmac.new(secret.encode(), external_id.encode(), hashlib.sha256).hexdigest()
// Web:
await EngageInApp.identify('user-123', { identityHash: hashFromYourServer });

// Flutter:
await EngageInApp.instance.identify('user-123', identityHash: hashFromYourServer);

The external_id you hash must be exactly the one you pass to identify. Requests with a missing or invalid hash are rejected. Rotating the secret invalidates every hash computed with the old one.

Older workspaces (created before verification became the default) keep it opt-in: it stays off until you generate a secret and turn on enforcement in the same settings card. We recommend turning it on.

7. Test & troubleshoot

  • Send yourself a campaign through the In-App channel, then open your app and call onAppForeground().
  • Nothing shows? Confirm you called identify() first, that a takeover isn't already on screen, and that a fetch isn't already in flight, since onAppForeground() no-ops in those cases.
  • Getting 401 'invalid identity verification'? New workspaces require identity verification. Generate the secret and pass identityHash (step 6).
  • Takeovers expire after the channel's lifetime if never fetched; reduce or extend it under the channel's display rules.
  • Delivered / shown / clicked events appear in your campaign analytics.