Getting Started with Overlay Messaging

Overlay Messaging enables your application to display personalised pop-up messages to customers across both web and mobile surfaces from a single Optimove campaign.

This guide covers how the channel behaves, then how to integrate it on Android, iOS, and Web.

Beta: Overlay Messaging is currently in beta. Tenant enablement is handled by your CSE — contact them to get access.


Prerequisites

Overlay Messaging is a cross-platform channel — both the Web SDK and Mobile SDK must be implemented to deliver messages across all surfaces. If only one SDK is integrated, messages will only reach customers on that platform.

RequirementDetailWebMobile
Optimove credentialsSee Credentials
Optimobile credentialsSee Credentials
Overlay Messaging enabled on tenantBeta — provisioned by your CSE. Contact them to enable.
SDK integrated with OverlayFull cross-platform delivery requires both; partial SDK implementation limits reach to a single surface
Domain and brand mappingDomains and brands must be mapped to your tenant before messages can be delivered on web. Configure this on the Domain Management page (see the Web SDK domain-management guide).
Web Push enabledOverlay Messaging is delivered over push infrastructure. Full Web Push SDK integration is not required — contact your CSE to have web push enabled.
Push provisioned and integrated in SDK (immediate messages only)On mobile delivery of immediate messages is triggered by silent push notifications. Overlay Messaging uses the same push integration as mobile push — your app calls the same push opt-in method to register for push and user push permission grant is required. See Allow push notifications.

How Overlay Messaging works

Read this section before integrating. The behaviour below is identical on Android, iOS, and Web — only the API surface differs.

Message types

There are two, and they behave differently throughout:

TypeWhat it isWhere it comes from
SessionOne message per session, fetched at session start.A scheduled campaign.
ImmediateDelivered in near real time in response to a customer action.A triggered campaign.

The distinction is between messages and campaigns, and both sets of terms are correct in their own place:

  • Messages are session or immediate. These are the values of the MessageType enum, and the terms used throughout this guide.
  • Campaigns are scheduled or triggered. That is what you set up in Campaign Builder, and the language the Academy article uses.

A scheduled campaign produces session messages; a triggered campaign produces immediate messages.

Sessions

A session defines the window during which a customer is eligible to receive one session message. It persists for the configured session length — default 1 hour, minimum 15 minutes — set in hours with sessionLengthHours or in minutes with sessionLengthMinutes.

A session starts when:

  • the SDK is initialised for the first time and has never loaded before. If it has loaded previously, the existing session continues;
  • the configured session duration expires;
  • the user ID is set or changed — for example a setUserId call, or any event that establishes the user ID;
  • resetSession() is called.

Because setting the user ID resets the session, you do not normally need to call resetSession() on login. Call it for other cases, such as logout.

Message limits

  • Session messages: one per session. Only the most recent eligible message is shown.
  • Immediate messages: not limited. Multiple may arrive within one session, each shown after the previous is interacted with or dismissed, provided each is still within its TTL (time to live) at the point of fetch.

Message delivery

There can be at most 1 message of each type (immediate/session) processed by SDKs simultaneously. For example,

  • if session expires while session message is already showing, messages will not stack: 2nd message will be delivered later.
  • If multiple triggers happen simultaneously, only 1 immediate message will be presented.
  • Session and immediate messages can form a stack of 2.

This ensures users will never see a long queue of messages to click through.

Session message delivery

At session start, the SDK fetches one session message meeting all of the following:

  1. Its scheduled time is in the past.
  2. It is still within its TTL, counted from scheduled time. Default TTL: 1 day.
  3. It is the most recent eligible message.

For example, with the current time at day 2, 12:00:

Campaign execution timeResult
Day 2, 12:03Not returned — execution time is in the future.
Day 1, 11:55Not returned — TTL expired.
Day 2, 11:55Returned.
Day 2, 11:30Not returned this session. Returned in the next one, if the customer is online after the current session expires and before the message reaches its TTL.

Immediate message delivery

  • TTL is 2 minutes from the time the message is sent.
  • Delivery is near real time: silent push on mobile, web sync on event emit.
  • On mobile, delivery requires push permission. Customers without push permission — or otherwise unreachable for push — will not receive immediate overlays.

Cross-device deduplication

When a message is interacted with or dismissed on one device, it will not be displayed on another.

Edge case: if a customer has the application open on two devices at the point of delivery, both may display the message before events propagate.

Interceptors

An interceptor runs your own logic before a message is presented, and decides whether it is shown. It is optional — with no interceptor set, messages display as normal.

Your interceptor receives the message and returns one of three outcomes:

ResponseEffect
showMessage is displayed to the customer.
deferMessage is left intact and can be fetched again in the next session.
discardMessage will not be delivered again.

If the interceptor does not resolve within its timeout it defaults to defer. Default timeout is 5,000ms, configurable via getTimeoutMs() on mobile or the timeoutMs option on web.

Testing caveat: an immediate message under test (including via Send Test) behaves like any immediate message. If your interceptor defers it, it is not re-delivered and expires after its 2-minute TTL. Immediate overlays are shown once, then gone. If a message needs to remain available for the customer to return to, use an Embedded Messaging inbox message instead.

Action handler (deep linking)

When a customer taps a CTA carrying a link, the SDK opens it for you — on web, in a new tab, with mailto:, tel: and sms: going to the device handler. The SDK can only open a link, so it cannot route inside your own application.

An action handler takes that over. You pass a partial object containing only the action types you want to own; the SDK dispatches against { ...defaults, ...yourOverrides }, so anything you omit — including action types added in future SDK versions — keeps its default.

linkAction is the only action type today.

Note: the action handler is independent of the interceptor. The interceptor decides whether a message is shown; the action handler decides what happens when a CTA is tapped.

Behaviour

  • Each setActionHandler call replaces your previous overrides — it re-merges over the SDK defaults, not over your last call. Pass undefined to restore every default.
  • Handlers fail closed. If your method throws, the SDK logs it and does not fall back to opening the URL — so a handler that allow-lists or sanitises a URL cannot be defeated by a bug in it.
  • Reporting and dismissal are unaffected. A handled tap is still reported for campaign analytics, and the overlay still closes if the message is meant to close.

Register your handler once at startup, before any message can be presented. Calling it before SDK initialisation finishes is fine.

Metrics

Reported in Mission Control:

MetricTrigger
SentCampaign was processed and is available for SDKs to fetch once it reaches its scheduled time.
DeliveredMessage was fetched by a device. May fire more than once if fetched by multiple devices before deletion.
OpenedMessage was closed by the customer — e.g. the (X) button, or hardware back on Android. Semantically: the customer saw the message.
ClickedCustomer interacted with the overlay — currently a CTA button or the close (X) button. Semantically: the customer clicked an interactive area of the message.

Rendering

On web, the overlay renders inside a Shadow DOM — CSS is fully isolated from the host page in both directions.


Integration

Initialisation

Enable Overlay Messaging when initialising the SDK.

Optimove.initialize(this, OptimoveConfig.Builder(
        "<YOUR_OPTIMOVE_CREDENTIALS>",
        "<YOUR_OPTIMOBILE_CREDENTIALS>")
    // ...
    .enableOverlayMessaging(OptimoveConfig.OverlaySettings(15, TimeUnit.MINUTES))
    .build());
Optimove.initialize(with: OptimoveConfigBuilder(
        optimoveCredentials: "<YOUR_OPTIMOVE_CREDENTIALS>",
        optimobileCredentials: "<YOUR_OPTIMOBILE_CREDENTIALS>")
    // ...
    .enableOverlayMessaging(sessionLengthHours: 1)
    // or .enableOverlayMessaging(sessionLengthMinutes: 15)
    .build())
// No SDK code changes are required on web.
// Contact your CSE to enable Overlay Messaging for your tenant.

Session reset

Resets the current session so the SDK can fetch the next available session message. Setting the user ID already resets the session, so this is for other cases — such as logout.

OptimoveOverlayMessaging.getInstance().resetSession();
OptimoveOverlayMessaging.resetSession();
optimoveSDK.API.overlayMessaging.resetSession();

Setting an interceptor

See Interceptors for what the three outcomes mean and how the timeout behaves.

OptimoveOverlayMessaging.getInstance()
    .setInterceptor(new OptimoveOverlayMessaging.OverlayMessagingInterceptor() {
        @Override
        public void onMessageLoaded(
            OverlayMessagingMessage message,
            OptimoveOverlayMessaging.OverlayMessagingInterceptorCallback callback
        ) {
            // Your logic here — call one of the following:
            callback.show();
            // callback.defer();
            // callback.discard();
        }

        @Override
        public long getTimeoutMs() {
            return 5000;
        }
    });

public class OverlayMessagingMessage {
    public enum MessageType {
        SESSION, IMMEDIATE
    }
    public long getId() { return id; }
    public JSONObject getContent() { return content; }
    @Nullable
    public JSONObject getData() { return data; }
    public MessageType getType() { return type; }
}
private class MyInterceptor: OverlayMessagingInterceptor {
    func onMessageLoaded(
        _ message: OverlayMessagingMessage,
        callback: OverlayMessagingInterceptorCallback
    ) {
        // Your logic here — call one of the following:
        callback.show()
        // callback.deferMessage()
        // callback.discard()
    }

    func getTimeoutMs() -> Int { return 5000 }
}

public struct OverlayMessagingMessage {
    public enum MessageType {
        case session
        case immediate
    }
    public let id: Int64
    public let content: NSDictionary
    public let data: NSDictionary?
    public let type: MessageType
}

OptimoveOverlayMessaging.setInterceptor(MyInterceptor())
// Show the message
optimoveSDK.API.overlayMessaging.setInterceptor(
    (message) => { return 'show'; },
    { timeoutMs: 10000 }
);

// Or take full control of presentation
optimoveSDK.API.overlayMessaging.setInterceptor(
    (message) => {
        customMessageQueue.push(message);
        return 'discard';
    },
    { timeoutMs: 10000 }
);

type InterceptorResponse = 'show' | 'defer' | 'discard';

type InterceptorCallback = (message: OverlayMessage) =>
    InterceptorResponse | Promise<InterceptorResponse>;

interface InterceptorOptions {
    timeoutMs?: number;
}

interface OverlayMessage {
    id: number;
    content: { ver: number; html: string };
    data?: Record<string, unknown>;
    type: OverlayMessageType;
}

enum OverlayMessageType {
    Session = 'session',
    Immediate = 'immediate'
}

Web only: setting overlay options

Change the session length at runtime. This takes effect immediately — if the new length means the current session has already elapsed, counting from the previous session start, a new session begins.

// In hours
optimoveSDK.API.overlayMessaging.setOverlayOptions({
    sessionLengthHours: 1
});

// Or in minutes
optimoveSDK.API.overlayMessaging.setOverlayOptions({
    sessionLengthMinutes: 15
});
interface OverlayOptions {
    sessionLengthHours?: number;   // min 1
    sessionLengthMinutes?: number; // min 15
}

Setting CTA action handler

See Action handler (deep linking) for the dispatch and failure rules.

MethodPayloadMeaningSDK default
linkActionLinkActionPayloadA CTA that navigates.Opens the URL — a new tab, or location.href for mailto: / tel: / sms:.

OptimoveOverlayMessaging.getInstance().setActionHandler(new OverlayMessagingActionHandler() {
      @Override
      public void onLinkAction(@NonNull Context context,
                                @NonNull OverlayMessagingMessage message,
                                @NonNull LinkActionPayload payload) {
          // For example, your own navigation instead of the default browser-open behavior
      }
});

public class OverlayMessagingMessage {

      public enum MessageType {
          SESSION, IMMEDIATE
      }
  
      private final long id;
      private final JSONObject content;
      private final JSONObject data;
      private final MessageType type;
  
      public long getId();
      public JSONObject getContent();
      @Nullable public JSONObject getData();
      public MessageType getType();
  }

  public final class LinkActionPayload {
      @NonNull public final String url;
  }
class MyOverlayActionHandler: OverlayActionHandler {
      func linkAction(message: OverlayMessagingMessage, payload: LinkActionPayload) {
          print("Link tapped: \(payload.url), message id: \(message.id)")
      }
}

OptimoveOverlayMessaging.setActionHandler(MyOverlayActionHandler())
  
// OptimoveOverlayMessaging.setActionHandler(nil)

public protocol OverlayActionHandler {
    func linkAction(message: OverlayMessagingMessage, payload: LinkActionPayload) throws
}
  
public struct LinkActionPayload {
    public let url: String
}

public struct OverlayMessagingMessage {
    public enum MessageType {
        case session
        case immediate
    }

    public let id: Int64
    public let content: NSDictionary
    public let data: NSDictionary?
    public let type: MessageType
}

optimoveSDK.API.overlayMessaging.setActionHandler({
    linkAction(message, payload) {
        myRouter.navigate(payload.url); // deep-link in-app instead of a new tab
    }
});

// Restore all SDK defaults
optimoveSDK.API.overlayMessaging.setActionHandler(undefined);

setActionHandler(overrides: Partial<OverlayActionHandlers> | undefined): void;

interface OverlayActionHandlers {
    linkAction(message: OverlayMessage, payload: LinkActionPayload): void;
}

interface LinkActionPayload {
    url: string; // resolved to an absolute URL by the SDK
}

interface OverlayMessage {
    id: number;
    content: { ver: number; html: string }; // message HTML, rendered by Optimove
    data?: Record<string, unknown>;          // optional campaign metadata
    type: 'session' | 'immediate';
}

Testing your overlays

You can test an overlay without building a campaign, using Send Test.

  • A Send Test behaves like a triggered campaign, so the message that arrives is an immediate message and follows immediate-message rules — including the 2-minute TTL.
  • If Overlay Messaging is enabled on both web and mobile, the test appears on whichever surface you open first. To test both, send one test and open it on the web, then send another and open it in the app.
  • On web, a Send Test needs a trigger event in place to surface the message — a page-load event, a login event, or any event the SDK emits.
  • On mobile, no trigger is needed (delivery is via push), but the customer must be opted in to push. If a mobile test doesn't appear, check push permission for that customer first.

Did this page help you?