Flow In-App vs. Targeting In-App
This guide walks you through what happens after you publish an in-app campaign: how your message reaches the user's phone, how long it takes, and what your application needs to do along the way.
SDK version noteThis guide describes the behavior of current SDK versions (Android 6.0.96 / iOS 5.101 and later). If your applications use an older Dengage SDK, some behaviors, especially fetch timing may differ; we recommend keeping the SDK up to date.
1. Dengage Has Two Separate In-App Systems
This is the most important thing to know before you build your first campaign: Flow In-App and Targeting In-App run through the same SDK, but they are two completely separate systems. Both end up showing an in-app message on the user's screen; the difference is where the decision about "who should see it" is made.
Flow In-App
The decision is made on the server, when you publish the campaign. A separate message is produced for each user, placed into a personal mailbox, and delivered once.
Think of it as a personal letter written in your name and dropped into your mailbox. You open the mailbox once, the letter is yours, and the mailbox is emptied.
Targeting In-App
The decision is made on the device, at the moment of display. A single campaign bundle is distributed to all devices; the rules are evaluated on the phone.
Think of it as the neighborhood notice board. Everyone looks at the same board; whether a notice speaks to you is decided by you, on the spot.
| Flow In-App | Targeting In-App | |
|---|---|---|
| Who decides | Server, the audience is computed at publish time | Device, the SDK evaluates the rules on the fly |
| Produced per person? | Yes, a separate message for each user | No, one bundle for everyone. Personalization is possible, but at display time rather than at production time, see section 5.4 |
| Where it's stored | Per user mailbox on the server | Single per app bundle on the CDN |
| Delivery semantics | Delivered once, removed from the mailbox once received | Continuous broadcast, everyone downloads it while it's active |
| Trigger | Campaign send time (segment) | The user's behavior at that moment |
You can and should use both at the same time; the SDK merges them into a single list and orders them by priority.
The Same Seven Steps, Side by Side
Chapters 4 and 5 follow the same seven steps, so you can compare the two systems step by step.
| Step | In Flow | In Targeting |
|---|---|---|
| Preparation | Content, audience, target apps, expiry | Content, rules, target app, date range |
| Audience resolution | Audience is resolved on the server | A rule set is defined; the decision is left to the device |
| Production | Per person messages are rendered | All of the app's active campaigns are compiled into one bundle |
| Distribution | Written into per user mailboxes | Published via the CDN |
| Delivery | The SDK reads and empties the mailbox | The SDK downloads the bundle |
| Selection | Device filters + priority | Device rules + priority |
| Measurement | Delivered + display/click/dismiss | Display/click/dismiss |
2. Concepts You'll See Throughout This Guide
| Term | Meaning |
|---|---|
| Account | Your Dengage account. Each account's data is kept fully separate. |
| App | A platform-based application definition (iOS, Android, Web). Many in-app settings are per app. |
| Contact | A known, identified user. Identified by contactKey. |
| Device | An unidentified (anonymous) device. Identified by deviceId. |
| Send | A single send instance of a campaign. |
| expireDate | The message's last moment of validity. After this moment it is not shown and is deleted automatically. |
| priority | Which message is shown when more than one is eligible at the same time. Lower number = higher priority. |
| displayCondition | Where / to whom the message is shown: screen name filters, rule set. |
| displayTiming | When / how many times the message is shown: delay, repeat interval, maximum display count. |
| SDK Parameters | The settings package that determines the SDK's behavior. The SDK pulls it from the server and caches it. |
3. When Does the SDK Go to the Server? - The Fetch Mechanism
The real answer to "I sent the campaign, when will the user see it?" lives here, and it applies to both systems. Even if your campaign is ready on the server, the message only lands on the device when the SDK goes to the server.
3.1 The Basic Principle
The SDK does not continuously poll the server in the background. Going to the server requires two conditions to hold together:
1) A TRIGGER must occur (when is it attempted)
+
2) The FETCH INTERVAL must (is the attempt allowed)
have elapsed
↓
the SDK goes to the server
If a trigger occurs but the interval has not elapsed, the call is silently skipped, this is not an error; it is by design. The main exception is foregrounding: launching the app and returning from the background always produce a fetch regardless of the interval (only a 10-second floor is applied, to filter rapid back-to-back transitions). The interval rule applies to the in-session timer.
3.2 Triggers
| Trigger | Description |
|---|---|
| App comes to the foreground | The dominant trigger. First launch and return from background always produce a fetch, they are not subject to the interval; only the 10-second floor applies. The vast majority of messages arrive at this moment. |
| SDK settings refresh | The SDK refreshes its settings package at every app start (when the first screen opens); once the settings arrive, a fetch is triggered right after. |
| In-session timer | If the app stays in the foreground, recurring rounds follow the fetch interval; on empty responses the rounds gradually become less frequent (see 3.4). The timer stops in the background, and is reset and re-armed on foregrounding. |
In-app is not a push notification. No in-app message is shown unless the user opens the app, if a push arrives and the user does not tap it and enter the app, no in-app message appears. Events that wake the app in the background (e.g. silent push) do not produce an in-app request either, fetch only runs while the user is actually on screen. A background wake-up also does not count as a session/visit; the session window slides based on inactivity, so visit-based Targeting rules evaluate accurately.
3.3 Fetch Intervals and Where You Configure Them
| Fetch type | Setting | Where | Scope |
|---|---|---|---|
| Flow in-app messages | inAppFetchIntervalInMin | Panel → application settings | Per app |
| Minimum time between messages | inAppMinSecBetweenMessages | Panel → application settings | Per app |
| Targeting campaign bundle | realTimeInAppFetchIntervalInMinutes | System-wide configuration | Global, cannot be changed per app |
| Visitor information | - | Fixed inside the SDK | Freshness of a few minutes |
- For Flow, how quickly messages are seen is under your control, per app.
- For Targeting there is no such flexibility; the interval is shared across the whole system.
- Shortening the interval has a cost: more frequent requests mean more battery and data use on the device.
A commonly used Flow fetch interval is in the neighborhood of 15 minutes; this is not a constant, however, it is an application setting.
Fetch intervals, the minimum time between messages, and validity limits vary by account and application, check the application settings in your panel for the values that apply to you.
3.4 How the Interval Works
After every fetch, the SDK sets a stamp saying "the next fetch is no earlier than this moment". The in-session timer checks this stamp first; foregrounding does not, apart from the 10-second floor, it always fetches.
Example: 15-minute interval
09:00 App launched → FETCHES (every launch is unconditional)
09:03 Background → foreground → FETCHES (10-second floor has elapsed)
09:03:05 Another quick switch → 10-second floor → SKIPPED
09:15 User is in the app → in-session round → FETCHES
Gradual back-off on empty responses: as in-session rounds keep coming back empty, the SDK gradually spaces them out; the ceiling is the greater of your fetch interval and 15 minutes. When a non-empty response arrives, the cadence returns to normal; foregrounding resets the back-off entirely. Users with a waiting message never feel this delay, foregrounding fetches unconditionally anyway.
On Error
- A failed request also consumes the window; the next attempt happens when the interval elapses or at the next foregrounding.
- While a request is in flight, a second one of the same type is not sent.
3.5 What Bypasses the Interval and What Doesn't
| Situation | Bypasses? | Why |
|---|---|---|
| The user keeps navigating inside the app | No | While a session is running, the only fetch path is the in-session timer, and it is subject to the interval; on empty responses the rounds can space out up to the back-off ceiling (see 3.4). Screen transitions (setNavigation) do not trigger a fetch, they only evaluate the display of messages already on hand. |
| The app is brought to the foreground from the background | Yes | Foregrounding is not subject to the interval; only the 10-second floor applies (0 in development mode). |
| The app is fully closed and reopened | Yes | The first fetch of every launch is unconditional, the most reliable path for testing. |
| The app is uninstalled and reinstalled | Yes | The local store is reset, but so is all in-app state on the device (display counts, message list). |
| Development mode is on | Yes | Disables the intervals. See below. |
| The device is on the panel's "debug device" list | Yes | Listed devices behave like development mode: intervals and the floor are bypassed, and debug logs are produced in addition. |
Development Mode
You can enable development mode in two ways: from the application code (Android setDevelopmentStatus(true); on iOS, marking the environment as development) or from the panel, by adding the device to the debug device list (debugDeviceIds), the device adopts the mode at the next SDK settings refresh (app launch); no build change is required.
In development mode the SDK does not apply: the Flow fetch interval, the Targeting fetch interval, the 10-second foregrounding floor, or the minimum time between messages. In other words, every trigger really goes to the server, and messages can be shown back to back. When testing, this is essential.
The panel debug list also produces a step-by-step debug log of rule evaluation. Remember to remove test devices from the list before going live, listed devices skip all intervals.
3.6 Stale-Data Protection: Shared by Both Systems
Before showing a message, the SDK checks when the last successful fetch happened. The threshold: 4× the fetch interval, with a minimum of 60 minutes.
The rule is: if the last successful fetch on both the Flow side and the Targeting side is older than this threshold, no message is shown at all, neither Flow nor Targeting. If either one is fresh, there is no blocking.
The purpose is to prevent a device that has been offline for a long time from showing a campaign that has long since been cancelled or expired. Once connectivity returns and the first fetch succeeds, everything returns to normal.
3.7 How the Message List on the Device Is Preserved
- Messages downloaded to the device are kept in persistent storage, they are not lost when the app is closed.
- When a new fetch arrives, the list is not reset but merged: if the same message arrives again, its display count, next-display time and dismiss count are preserved.
- For a new message not present in the list, these values are restored from the display history kept on the device. Even if a message is deleted and downloaded again, the fact that "this user has seen it 3 times" is not lost.
- The display history is retained for a limited period and cleaned periodically.
3.8 Device Cleanup: Expired and Cancelled Campaigns
The local list on the device is kept tidy in two ways:
- Expired messages are filtered out on the device via
expireDate, they stop showing the moment their validity ends and are removed automatically. - Cancelled campaigns are removed shortly after cancellation: with each fetch, the SDK also learns which sends have been cancelled and drops the matching messages from its local list.
What to expect: a cancelled campaign is cleaned off the device at the device's next fetch.
4. Flow In-App: From Publish to Screen
4.1 Step 1 · Preparation: Set Up the Campaign in the Panel
- Content: an HTML-based template (modal, full-screen, banner, inline, story, countdown). Define your personalization variables here.
- Target applications: multi-select. Determines which "mailboxes" the message will be written into.
- Target audience: segment / list / filter.
- Validity: a fixed date, or "N days/hours/weeks from send".
- Display settings:
priority,displayTiming,displayCondition. - Send time: immediately or scheduled.
The target application list intersects with the target audience. If a contact is in the audience but has no registered device in the selected applications, no message is created for that user.
4.2 Step 2 · Audience Resolution: Who Gets the Message Is Decided on the Server
When you publish, the campaign turns into a send order. When it is due, the audience is resolved: for every contact/device in the target segment, a personal message is prepared.
What to expect: from seconds to minutes depending on audience size. For audiences in the millions, the first messages start flowing while the rest are still being produced, the process is a stream.
4.3 Step 3 · Production: Messages Are Personalized
For each target user, the content template is rendered with the personalization variables and prepared for delivery. Content that is identical for everyone is stored once and attached at delivery time, so even very large sends stay fast.
4.4 Step 4 · Distribution: Messages Are Written into User Mailboxes
Each message is written into the user's own "mailbox" on the server.
| Behavior | What it means for you |
|---|---|
| Per-app isolation | A separate mailbox is used for each target application. A message shown on iOS can also appear on Android, each platform reads from its own mailbox. |
| Device intersection | For contact records, the campaign's target applications are intersected with the applications where the user actually has a device. If the intersection is empty, no message is created. This is the most common cause of "the audience had 1M people but only 600K messages were created". |
| Per-message validity | Every message lives with its own expireDate. An expired message is deleted on its own; the others remain. |
| Reliability | Messages are not lost during temporary hiccups, they are retried until they are safely written. |
What to expect: distribution typically takes seconds.
4.5 Step 5 · Delivery: The Message Moves from Server to Device
- The SDK's fetch request is validated (account, application, in-app enabled).
- The mailbox is read and emptied in a single operation, the same message is never delivered twice.
- Delivered messages are recorded as delivered; this feeds the delivered metric you see in reports.
If a message cannot be completed at delivery time, it is not lost, it stays in the mailbox and is delivered on a later fetch. A read-only counterpart, peekMessages, shows the mailbox contents without emptying it (useful for diagnostics, see Chapter 8).
4.6 Step 6 · Selection: The Device Decides Which Message to Show
The display decision is made when setNavigation() is called, which means your application must inform the SDK on every screen transition.
-
Stale-data protection (see 3.6)
-
Expiry filter: messages past their
expireDateare discarded. -
Minimum time between messages:
inAppMinSecBetweenMessages. -
Screen filter: evaluated in two passes. Screen name matching (combinable with AND/OR), but elimination is not single-pass:
Situation What is considered No screen name provided Only messages without a screen filter Screen name provided First, messages with a screen filter that matches Screen name provided, none match Fall back to messages with no screen filter at all In practice: setting up a screen-filtered campaign does not prevent unfiltered campaigns from appearing on that screen, it only takes precedence over them.
-
Display timing validation:
maxShowCount,showEveryXMinutes. -
Priority ordering:
-
Flow messages come before Targeting messages
-
Then
priority, lower number first -
If priorities are equal, the remaining ties are resolved automatically (rule set and expiry are considered)
### When it matters which of two messages appears first, give them distinct `priority` values rather than relying on tie-breaking.
-
-
Delay:
displayTiming.delay. -
Special content types: inline, story, countdown, coupon messages. If a coupon cannot be allocated, the message is not shown.
After display: if showEveryXMinutes is defined, the message stays in the local list and is scheduled for the next round. If it is not defined, the Flow message is deleted, that is, a Flow message is shown once by default. For the full semantics of the frequency settings, see 6.4.
4.7 Step 7 · Measurement: Events Are Reported
| Event | When |
|---|---|
| Delivered | When the server delivers the message to the SDK |
| Displayed | When the message appears on screen |
| Clicked | When the button / message is tapped |
| Dismissed | When the user closes it |
4.8 Test Sends
When you use "send test" in the panel, the audience resolution step is skipped: the message is prepared immediately. What to expect: the test message lands in the mailbox within seconds; when it appears on the device depends entirely on the fetch mechanism.
- Install a build with development mode enabled on the test device, the fetch interval and the between-messages wait are disabled.
- If the message does not arrive, fully close (force-quit) the app and reopen it, the first fetch of every launch is unconditional; this is the most reliable path. If you test with background/foreground cycles, leave at least 10 seconds between two foregrounds, a transition made before the floor elapses produces no fetch and creates the false impression that "the message didn't arrive". In development mode and for devices on the panel debug list this floor is 0.
A test send cannot be delivered to a contact that does not have a registered device. Before sending a test, make sure the subscription process has been completed for that contact, and send the test to the app where the subscription was created.
⚠️ What a Test Send Validates and What It Doesn't
A test send is a content preview, not a delivery simulation. The payload is built from scratch, and the campaign's display settings are not carried into it, default/fixed values are used instead.
| Validates ✅ | Does not validate ❌ |
|---|---|
| The content HTML (with personalization variables rendered) | displayTiming : delay, repeat interval, postpone allowance: all defaults |
| The screen name filter you type on the test screen | The campaign's displayCondition rules: sent empty |
| The message reaching the device and the SDK integration | Validity: fixed at 1 hour, not the campaign's expire setting |
| The template rendering correctly on the device | Priority: fixed, not the campaign's priority setting |
In practice this means: nothing about display behavior can be tested with a test send; delay, repetition, "remind me later", screen filters, priority races. Make a real send for those. Concluding "it doesn't work" from a test send is the most common false alarm.
4.9 Campaign Cancellation
- Server cleanup: the messages belonging to the cancelled send are removed from the mailboxes; messages not yet delivered never go out.
- Device cleanup: the SDK learns about the cancellation and drops the messages it has already downloaded (see 3.8).
Cancellation cannot take back messages that have already been displayed.
4.10 The Flow In-App Pipeline at a Glance
PREPARATION Panel: campaign + audience + target apps + expire
│
▼ publish
AUDIENCE Send order ──► audience resolution (streaming)
│
▼
PRODUCTION Personalization + content rendering
│
▼
DISTRIBUTION Per-user mailboxes, per-message validity
│ └─ for contacts: target apps ∩ apps with a device
▼
DELIVERY SDK ──fetch──► mailbox is read and emptied,
│ delivery is recorded
▼
SELECTION Device: local list → expire/screen/timing filters
│ → priority → delay → SHOW
▼
MEASUREMENT displayed / clicked / dismissed → reports
5. Targeting In-App: From Publish to Screen
5.1 Step 1 · Preparation: Set Up the Campaign in the Panel
In a Targeting campaign you do not select an audience. Instead, you define:
- Target application(s): which application's broadcast bundle the campaign will enter.
- Content: the same template infrastructure as Flow.
- Rule set: the conditions to be evaluated on the device (see 5.2).
- Screen filters: on which screen(s) it applies.
- Start / end date: the campaign stays live within this range.
- Display settings:
priority,displayTiming. In Targeting,showEveryXMinutesis used much more commonly. - Activation: set the campaign to active or passive. Targeting in-app campaigns have no approval mechanism; activating the campaign is enough for it to go live. (In Dengage, the approval process applies to campaign Flows and contents; Targeting in-app campaigns are not part of it.)
5.2 Step 2 · Targeting: The Decision Is Left to the Device
| Group | Parameters |
|---|---|
| Behavior | Cart item count, cart total, category path, last viewed product and its price, current page type/title, event history |
| Visit | Pages seen in the visit, visit duration, first visit or not, visit count, last visit time |
| Identity | Anonymous or not, segment, tag, contact attributes |
| Device / context | OS and version, brand/model, screen dimensions, language, timezone, country/city/region, push permission |
| Time | Hour, day of week, month |
| Free-form | Values your application passes via setNavigation |
Some of these parameters are known directly on the device (device, time, screen); some come from the calls your application reports to the SDK (cart, page, events); segment/tag/contact attributes are retrieved from the server with a separate visitor-information call (see 5.7).
5.3 Step 3 · Production: The Application's Bundle Is Compiled as a Whole
Targeting's unit of publication is not the campaign but the application. Each application has a single broadcast bundle, and that bundle is the complete list of all campaigns currently live for that application.
When a campaign is added, edited, stopped, or expires, the bundle is regenerated as a whole. The device downloads a single file and learns "everything that is live right now", so there are no missed updates and no drift between devices. A few practical consequences:
- Even a small edit republishes the bundle; allow a short propagation time (see 5.5).
- One campaign with a content problem does not affect the others in the bundle.
- Delivery cost does not depend on how many users you have, everyone downloads the same file.
5.4 Personalization: How It's Done in Targeting
The Targeting bundle arrives identical on every device. There is no per-person render step while the bundle is prepared, so contact variables written into the template (name, birth date, etc.) are not filled in when the bundle is compiled.
Personalization happens instead on the content side, at display time. The SDK provides a bridge to the in-app content; through this bridge the content can fetch data from the server at the moment of display and fill the fields on screen. This means the message uses the data from the moment the user sees it, not from the moment the bundle was compiled.
Which Path, When
| Approach | When it fits |
|---|---|
| Rule-based splitting | The simplest path. If a few variants are enough ("A to those with a cart above 500 TL, B to the rest"), set up separate campaigns; no content development needed. |
| App-supplied values | Your application hands any key-value pairs to the SDK with setInAppDeviceInfo(key, value); the content template uses them directly. No network call required. |
| Coupons | Per person code distribution. The SDK allocates the coupon before display and injects it into the content. |
| Live data on the content side | When a truly personal value must be shown (name, points, amount, status). Requires development in the content template. |
| Flow In-App | When personalization is central to the campaign and delivery/reporting guarantees are also wanted. |
App-Supplied Values, dnInAppDeviceInfo
dnInAppDeviceInfoThe simplest and most overlooked path. Your application leaves values it already knows with the SDK; the SDK hands them to the content template at display time.
Application:
Dengage.setInAppDeviceInfo("firstName", "Alex")
Dengage.setInAppDeviceInfo("points", "1240")
Template:
Hello {{dnInAppDeviceInfo.firstName}}
You have {{dnInAppDeviceInfo.points}} points
You define the keys, there is no fixed list. The same API exists on both platforms (setInAppDeviceInfo, clearInAppDeviceInfo, getInAppDeviceInfo).
- Set the value before the message is displayed; otherwise the field renders empty.
- Values are stored persistently. If you do not call
clearInAppDeviceInfo()on logout, the previous user's name is shown to the next user, this is the most likely source of error. - For data your application does not know (e.g. the current points balance on the server), this path is not enough, use live data instead.
Example Scenarios Possible with Live Data on the Content Side
- "Hello Alex" , greeting the user by name
- "You have 1,240 points 260 more to reach Gold", loyalty status
- "10% off the 3 items in your cart", the cart at that moment
- "A price just for you: 349 TL" , personal pricing
- "Your order has shipped", order status
- "Picked for you", a personalized recommendation list
This approach requires development in the content template; since the data is fetched at display time, the content must also handle loading and error states. It also requires an up-to-date Dengage SDK. Assess feasibility per campaign together with your mobile team.
5.5 Step 4 · Distribution: The Bundle Is Published via CDN
The bundle is published to the CDN, and devices pick up the new version as it propagates.
What to expect: publishing takes seconds; total latency is determined by CDN propagation, typically seconds to a few minutes.
5.6 Step 5 · Delivery: The SDK Downloads the Bundle
The SDK downloads the application's bundle from the CDN. There is no per user query, every device receives the same file. Download conditions are governed by Chapter 3; the interval setting is system-wide.
SDK Compatibility
Not every device in the field runs the newest SDK. Campaigns that use the newest capabilities are delivered only to devices whose SDK can evaluate them correctly; older SDKs simply do not receive those campaigns. This prevents a campaign from being shown to the wrong people, the design prefers silent exclusion over wrong display. Capabilities that require an up-to-date SDK include:
- Rule criteria such as last viewed product id/price, last category path, current page title/URL/type, cart contents, and event history
- Coupon sections in the content
If your campaign uses one of these capabilities, users whose devices run an outdated SDK never see that campaign. If a campaign report shows lower reach than expected, check the SDK version distribution of your user base.
5.7 Visitor Information
If your rules use segments, tags, or contact attributes, the SDK retrieves this information with a separate call and caches it on the device with a freshness of a few minutes. If a user has just joined a segment, the campaign may not be visible to them until this information is refreshed. This information is used only in rule evaluation, not in content rendering (see 5.4).
5.8 Step 6 · Selection: The Device Evaluates the Rules
When setNavigation() is called, the SDK evaluates every campaign it has downloaded, one by one: is the date range valid, does the screen filter match, does every criterion in the rule set match the current value, is displayTiming satisfied, then the winner is chosen by priority order (see 4.6).
After display, a Targeting message is not removed from the local list, as long as the campaign remains in the bundle, it can be shown again whenever the conditions are met. This is the most visible behavioral difference from Flow.
DefinemaxShowCount = 1if you want a Targeting message to appear only once. The default in Targeting is unlimited; the limit is yours to set. Details and caveats: 6.4.
5.9 Step 7 · Measurement: Events Are Reported
Targeting display, click, and dismiss events are reported together with the session, campaign, and content identity. First-open and foregrounding signals are also sent; these feed rules such as first visit and visit count.
5.10 The Targeting In-App Pipeline at a Glance
PREPARATION Panel: campaign + rules + target app + date range
│
▼ save / activate
TARGETING Rule set is defined — the decision is left to the device
│
▼
PRODUCTION Compile: ALL active campaigns of the app → one bundle
│
▼
DISTRIBUTION Published via CDN (propagation: seconds – minutes)
│
▼
DELIVERY SDK ──bundle──► one file for every device
│ SDK ──visitor info──► segment, tag, contact attributes
▼
SELECTION Device: rule evaluation (cart, screen, visit, device, time…)
│ → priority → SHOW
▼
MEASUREMENT display | click | dismiss → reports
6. Comparison and Selection Guide
6.1 Side by Side
| Dimension | Flow In-App | Targeting In-App |
|---|---|---|
| Targeting decision | On the server, at publish time | On the device, at display time |
| Content personalization | Full, rendered per user on the server | Indirect, not done at bundle compile time; via app-supplied values, rule-based variants, coupons, or live data (see 5.4) |
| Targeting signals | CRM / segment data (full data) | Live signals on the device + segment/tag |
| Time to go live | Depends on audience size | Publish + CDN propagation |
| Fetch interval control | Per app, under your control | System-wide, cannot be changed |
| Delivery guarantee | High, stays in the mailbox until delivered | None, there is no concept of "missing" it |
| Repeat display (default) | Once, repeatable with showEveryXMinutes | Unlimited, capped with maxShowCount (= 1 → once). Details: 6.4 |
| Audience scale | Any size, messages are produced per user | Any size, every device downloads the same bundle |
| Anonymous users | Possible, but requires device registration | Natural |
| SDK version sensitivity | Low | High, campaigns using the newest criteria/coupons do not reach outdated SDKs |
| Data held on the device | Only the messages sent to that user | All active campaigns of the application |
| Cancel / rollback | Server cleanup + device cleanup | Deactivate + republish |
| Reporting | Delivered + Displayed + Clicked + Dismissed | Displayed + Clicked + Dismissed |
| Priority race | Flow wins when both are eligible | Steps in when there is no Flow message |
6.2 Which Should You Choose?
Choose Flow In-App if:
- The message text will contain person-specific information (name, order number, a specific product)
- Targeting is based on CRM / segment data
- You need "how many people did we deliver to" reporting
- The message must be delivered exactly once, with certainty
- You are distributing single-use coupons/codes
Choose Targeting In-App if:
- The trigger is the user's behavior at that moment
- The audience includes anonymous users
- The same message should be shown again whenever the conditions are met
- You want an "always on" campaign
- The audience is very large and the content has no person-specific text
- You want to switch it on/off quickly or A/B test
Personal values can be shown in Targeting too, but that requires development on the content side (see 5.4). If personalization is central to the campaign and you also want a delivery report, Flow is the better fit. If personalization is a small part of the message (a name, a points balance) and the campaign itself must be behavior-triggered, Targeting + live data on the content side is the better choice.
6.3 Scenario Examples
| Scenario | Recommended | Why |
|---|---|---|
| Reminder to a user who abandoned their cart | Targeting | The trigger is the instantaneous device state |
| Personal discount code for users on their birthday | Flow | Personal code + CRM data + single use |
| New version announcement, everyone, for 2 weeks | Targeting | No personalization, continuous broadcast |
| Gold segment with no purchase in 30 days | Flow | The segment is computed on the server; a delivery report is needed |
| Onboarding for first-time openers | Targeting | First-visit information lives on the device; not even a contact is required |
| Last call 1 hour before the deadline, to those who clicked | Flow | Targeting relies on past engagement data |
| Coupon for users lingering on the product detail screen | Targeting | The screen + duration combination is only known on the device |
| Sign-up prompt for anonymous users | Targeting | Anonymity is known on the device |
| "Hello {name}" personalized recommendations to millions | Flow | Template-variable personalization is only done in Flow (for Targeting, see 5.4) |
| Store opening announcement in a specific city | Targeting | The city is known on the device. If person-specific text is needed, Flow |
6.4 Display Frequency: How to Control It in the Two Systems
Frequency control uses the same two settings in both systems; the difference is in the default behavior.
| Setting | Value | Meaning |
|---|---|---|
maxShowCount | empty / 0 / -1 | Unlimited |
maxShowCount | N | Shown at most N times per device |
showEveryXMinutes | empty / 0 / -1 | No repeat interval |
showEveryXMinutes | N | At least N minutes must pass between two displays |
Flow: once. After the message is displayed it is deleted from the device's list → naturally once. If you want repetition, define showEveryXMinutes.
Targeting: unlimited. As long as the campaign remains in the bundle it is not deleted from the list → naturally unlimited. If you want a limit, define maxShowCount.
Common Recipes
| You want | maxShowCount | showEveryXMinutes |
|---|---|---|
| Show only once in Targeting | 1 | empty |
| Once a day in Targeting | empty | 1440 |
| At most 3 times, 1 hour apart, in Targeting | 3 | 60 |
| Once in Flow | - | Show once in the panel |
| Every other day in Flow | - | Show every in the panel, 1 day |
| "At most 3 times" in Flow | Not possible | Use a shorter validity (expire) to limit repeats indirectly |
The panel presents repetition for a Flow campaign not as a number but as a binary choice:
- Show once → the message is deleted from the device after display; it appears once
- Show every … → the message repeats at the chosen interval until
expireDateis reached
Consequently, a count-based limit like "show 3 times" is only possible in Targeting (maxShowCount); in Flow, repetition is limited by the validity period.
Things to keep in mind:
- The counter is per device, not per user. If the same person signs in from two devices, they can see the message twice. In Flow there is no such risk, because the mailbox is per person + application.
- Uninstalling and reinstalling the app resets the counter, like all in-app state on the device.
- If a campaign is deactivated and reactivated, the message drops off the device's list; when it comes back, the counter is restored from the display history. If the campaign is republished after a long gap, the counter may start fresh.
6.5 "Remind Me Later" and maxDismissCount
Some in-app templates include a third "Remind me later" button in addition to the normal action and close buttons. maxDismissCount is the counter side of this feature, it is not a standalone "dismiss limit".
Where You Configure It
| Where | What you do |
|---|---|
| Content editor | Give the third (tertiary) button the Dismiss action type; label it e.g. "Remind me Later" |
| Send settings | Select Show every; the two fields that appear under it govern the behavior |
| Panel field | Function |
|---|---|
| Show every N hours | How much later the message is shown again |
| Max dismiss count | How many times the user can postpone |
How It Works
- The message is displayed → the SDK schedules the next display per the Show every interval and keeps the message in the local list.
- The user taps "remind me later" → the postpone counter increments, and the message is not deleted.
- When the time elapses, the message is shown again.
- When the counter reaches Max dismiss count, the message is deleted permanently.
The postpone interval is the campaign's Show every interval, there is no separate postpone duration.
Closing is different. The content editor has separate Close Button and Dismiss On Touch Outside options. These do not increment the postpone counter and delete the message unconditionally, which is correct behavior by design.
⚠️ Important: Max Dismiss Count Defaults to 0 and 0 Does Not Mean Unlimited
A value of 0 is treated as "delete on the first tap". So if you select Show every but leave Max dismiss count at its default of 0, tapping "remind me later" does not postpone the message, it deletes it. When using the button, always set Max dismiss count to 1 or higher.
Also note:
- With Show once, the message is deleted at display anyway, the "remind me later" button becomes meaningless in that mode. Campaigns using the button must select Show every.
- A test send does not carry the campaign's display settings (see 4.8), including Max dismiss count. Validating the postpone behavior requires a real send.
- In Targeting, the postpone counter does not apply: Targeting messages stay in the local list anyway and simply follow the
maxShowCount/showEveryXMinuteslimits.
6.6 When Using Both Together
- Priority: If both are eligible on a screen, the Flow message is shown.
- Frequency:
inAppMinSecBetweenMessagesis a shared ceiling for both systems. - Design advice: Give Targeting campaigns that stay live continuously a high
prioritynumber (low priority) so they do not suppress Flow messages. - Monitoring: For Flow, watch the delivered → displayed conversion. A large gap usually means "the message was delivered but the application did not call
setNavigation".
7. What Your Application Must Do
7.1 Mandatory Steps
| Step | Why it is critical |
|---|---|
| Initialize the SDK, configure the API endpoints | The in-app, event, and Targeting endpoints are configured separately |
setContactKey, set on login, clear on logout | Flow messages are addressed by the user key. Wrong/empty key = no messages |
setNavigation on every screen | Display is evaluated only on this call. If it is not called, even a downloaded message never appears |
| Write screen names exactly as in the panel | Screen filters rely on exact matching |
| Enable development mode in development builds | The only mechanism that makes the test cycle possible (see 3.5). Must be off in production builds |
7.2 Feeding the Targeting Rules
| Call | What it feeds |
|---|---|
pageView | Pages seen in the visit, page type/title, last category |
setCart · addToCart · viewCart | Cart item count, cart total, cart contents |
setCategoryPath | Category path |
setCity · setState | City / region |
sendCustomEvent · order · search | Event-history-based rules |
setTags | Tag rules |
setNavigation(..., params) | Free-form parameters usable in rules |
7.3 Optional Capabilities
- Inline in-app: a message area embedded in the screen; matched via
propertyId. - Story: a story list component; the seen state is kept on the device.
- Test page: an SDK screen showing the messages currently on the device and their states.
- Deeplink: deeplinks on message buttons are passed to your application.
7.4 Common Integration Mistakes
setNavigationis called only on the home screen → no message ever appears on other screens. This is the most frequent problem.- Screen name inconsistency →
ProductDetailin the panel,product_detailin the app. - The contact key is not cleared on logout → the previous user's messages are shown to the new user.
- Expecting messages before device registration completes → if the contact has no device in the target application, the Flow message is never created.
- Testing with development mode off → fetch intervals can block your test cycle. Fully close and reopen the app (every launch fetches) or enable development mode.
inAppMinSecBetweenMessagesset too high → the second message never appears in the same session.- Writing contact variables into a Targeting template → they are not filled at bundle compile time and stay empty. Use one of the paths in 5.4 for personalization.
8. Troubleshooting: "The Message Doesn't Appear"
Work through the steps in order; each step assumes the previous one.
- Was the campaign really published?
- Flow: Is the send status complete? Is the created message count in line with expectations?
- Targeting: Is the campaign active? Is it within the date range?
- Does the audience / rule cover this user?
- Flow: Is the user in the segment? And do they have a registered device in the target applications?
- Targeting: Does the device actually have a value for every criterion? If a segment/tag rule is used, has the visitor information been refreshed?
- Is the message in the server-side mailbox? (Flow only)
- Check with
peekMessages, it shows the mailbox without emptying it. If the message is there, the problem is on the device side; if not, the issue is in production/targeting.
- Check with
- Did the device download the message? (the most frequent root cause)
- Did the app come to the foreground? Fully closing and reopening the app always produces a fetch (see 3.5); if the message still does not arrive, the problem is not the interval.
- If testing with background/foreground cycles: was there at least 10 seconds between the two foregrounds? A transition before the floor elapses produces no fetch (the floor is 0 in development mode / on the panel debug list).
- Is the contact key correct? Is the application identity correct?
- Targeting: Has the CDN propagation completed?
- Targeting: Does the campaign use a capability that requires an up-to-date SDK, and is the SDK on the device current? (see 5.6)
- Could the device have filtered the message out?
- Is
expireDatepast? Does the screen name match the filter? IsmaxShowCountexhausted? Has theshowEveryXMinutesinterval arrived? (These counters survive app restarts, see 3.7) - Could
inAppMinSecBetweenMessagesbe suppressing it? Could stale-data protection have kicked in? (see 3.6) - Was another, higher-priority message shown instead? For coupon messages: could the coupon allocation have failed?
- Is
- Could cancellation / cleanup have intervened?
- If the campaign was cancelled or has expired, the device may have discarded the message (see 3.8).
- The report shows delivered but no displayed
- The message arrived but the display conditions were never satisfied, most often a screen filter or a missing
setNavigation.
- The message arrived but the display conditions were never satisfied, most often a screen filter or a missing
Diagnostic Tools
| Tool | What for |
|---|---|
peekMessages | Seeing the mailbox contents without disturbing it |
| Development mode | Disabling fetch intervals and the between-messages wait for fast testing |
| Panel debug device list | Putting a device into development mode remotely (intervals bypassed) + a step-by-step record of rule evaluation |
| SDK test page | The current message list on the device |
9. Timing Expectations
| Step | Flow In-App | Targeting In-App |
|---|---|---|
| Publish → message/bundle ready | Seconds (small) – minutes (audiences in the millions) | Seconds |
| Ready → available for devices | Seconds | CDN propagation: seconds – a few minutes |
| Available → on the device | Next fetch, foregrounding (instant) / in-session round (per-app interval) | Next fetch, foregrounding (instant) / in-session round (system-wide interval) |
| On the device → on screen | Next setNavigation + displayTiming.delay | Same |
| Test send | Seconds (audience step skipped) + fetch | Via preview |
| Cancel → removed from the device | At the device's next fetch | Republish + next download |
The biggest item in this chain is usually the user's next app open, the message comes down at the moment of opening, without waiting. For users who stay inside the app, the fetch interval is the deciding factor; for Flow you can manage it from the application settings.
9.1 What You Want → Which Setting
| If you want | What to do |
|---|---|
| The message to arrive as fast as possible | On foregrounding the message already arrives instantly. For users staying inside the app, lower (minimum interval for fetching messages) inAppFetchIntervalInMin Raise (minimum duration between displaying messages) inAppMinSecBetweenMessages. Configure here: iOS · Android · Huawei |
| The user not to see messages back to back | Raise inAppMinSecBetweenMessages. Note: if several messages are eligible at once, priority ordering decides and the highest-priority one is shown. But a higher-priority message arriving after another was shown still waits out this interval, regardless of its priority Configure here: iOS · Android · Huawei |
| The message to appear only once | Flow: Show once. Configure dismiss behaviour · Targeting: maxShowCount = 1 Configure delivery settings. |
| The message to repeat at intervals | Flow: Show every N Configure dismiss behaviour · Targeting: showEveryXMinutes = N Configure delivery settings. |
| A limited number of repeats | Targeting: maxShowCount. Not directly possible in Flow, limit indirectly by shortening the validity period |
| The user to be able to say "remind me later" | Dismiss type on the third button in content + Show every + Max dismiss count ≥ 1 in the send settings (see 6.5) |
| The message to appear on specific screens | A displayCondition screen filter; make sure the app calls setNavigation on that screen |
| The display to be slightly delayed | displayTiming.delay |
| Person-specific information in the text | Use Flow (see 5.4 for the Targeting alternatives) |
| A reaction to live behavior | Use Targeting |
| A faster test cycle | Add the device to the panel debug device list, or install a build with development mode on (see 3.5); background/foreground cycles or fully closing and reopening produce a fetch every time. In normal mode leave at least 10 seconds between two foregrounds |
9.2 Worked Scenarios
The examples below are for an application configured with inAppFetchIntervalInMin = 5 minutes and inAppMinSecBetweenMessages = 300 seconds (5 minutes).
Important: These two counters run in parallel; they do not add up. The fetch stamp is set at every fetch, the display stamp at every display, separately.
Scenario A: Single message; the user opens the app
09:00 Fetch happened; no message on the server
09:01 Campaign published; message ready
09:04 User opens the app → fetch (unconditional) → message arrives
09:04 setNavigation → wait out the delay → DISPLAYED
Result: at the next open. Since foregrounding is not subject to the interval, the message arrives at the first open after publishing; the fetch interval creates no wait in this scenario. The between-messages interval does not come into play either, no message was shown before.
Scenario B: Two messages arriving in the same round
09:05 Fetch → A and B arrive together
09:05 A displayed → next display allowed at 09:10
09:07 Screen change, setNavigation → B eligible but SUPPRESSED until 09:10
09:10 Next setNavigation → B DISPLAYED
Result: B five minutes after A. The between-messages interval is decisive. B also needs another setNavigation call to be displayed, it does not pop up on its own when the timer elapses.
Scenario C: The user is already in the app; the message is published meanwhile
09:00 User opens the app → fetch; nothing there
09:03 Campaign published; message ready
09:05 In-session round (interval: 5 min) → fetch → message arrives
09:05 setNavigation → DISPLAYED
Result: ~2 minutes. The in-session round follows inAppFetchIntervalInMin. If empty rounds had accumulated beforehand, the round may have spaced out somewhat (see 3.4); if the user backgrounds and foregrounds the app, the message still arrives instantly.
Scenario D: The message is published right after a fetch
09:00 A displayed → display allowed again at 09:05
09:06 Campaign B published
09:06 User opens the app → fetch → B was not ready yet; came back empty
09:08 Background → foreground → fetch → B arrives → DISPLAYED
Result: at the next foregrounding. An empty fetch does not "lock" the window, the next foregrounding (beyond the 10-second floor) tries again; if the user stays inside the app, B arrives on the next in-session round. The display-side wait (inAppMinSecBetweenMessages) applies separately.
9.3 Common Situations
| Situation | Cause |
|---|---|
| "The message arrived but was displayed late" | Arrival and display are separate: it may be waiting on inAppMinSecBetweenMessages or on a new setNavigation (Scenario B) |
| "I'm sitting in the app and the message never comes" | The in-session round follows the interval but can space out up to the back-off ceiling on empty rounds (see 3.4). Backgrounding and foregrounding the app produces an instant fetch |
| "I sent a test and it didn't land on the device" | Fully close and reopen the app, every launch produces a fetch (see 3.5). If it still doesn't arrive, check the contact/application identity and the targeting (see 4.8). Adding the device to the panel debug list speeds up testing |
| "The message arrived but doesn't show" | setNavigation isn't being called, the screen filter doesn't match, or the between-messages interval is suppressing it (see Chapter 8) |
| "The second message never appeared" | A new setNavigation is needed after the interval elapses; it is not shown on its own when the timer expires |
| "No in-app has appeared for an hour" | Stale-data protection may be active, if the last successful fetch is older than 60 minutes, all display stops (see 3.6) |
Updated 2 days ago