Skip to main content

In-App Messaging

In-app messages are messages shown inside your mobile or web application while a person is actively using it. Unlike push notifications (which appear on the lock screen), in-app messages render as modals, banners, inline blocks, or inbox cards within your app's UI.

In Conviso, you can send in-app messages in two ways:

  1. Broadcasts — One-time messages to a segment or audience.
  2. Campaigns — Automated messages as part of a journey workflow.
Transactionals

In-app messaging is not available on Transactionals today. Use Campaigns or Broadcasts, or trigger a journey programmatically via events.


How in-app messaging works​

In-app delivery is SDK-driven. Conviso does not render messages inside your app for you — your application fetches eligible messages and displays them using your own UI components.

At a high level:

  1. Queue — When a broadcast is sent or a campaign step runs, Conviso creates an in-app delivery for each recipient.
  2. Sync — Your SDK calls the Data Gateway /sync endpoint (with the current screen name) to fetch messages that are eligible right now.
  3. Display — Your app renders the message using the render_type and content returned by sync.
  4. Track — Your SDK reports impression, click, and dismiss events so Conviso can update analytics and delivery status.

For near-real-time delivery, the SDK can also open a Server-Sent Events (SSE) stream. When a new message is queued, Conviso pushes a sync event so the SDK refetches immediately instead of waiting for the next poll.

Author (Dashboard)          Conviso Backend              Your App (SDK)
| | |
| Create broadcast/campaign | |
|--------------------------->| |
| | Queue delivery (MongoDB)|
| |--------------------------->|
| | SSE: event sync (optional)|
| |--------------------------->|
| | | GET /sync
| |<---------------------------|
| | Eligible messages |
| |--------------------------->|
| | | Render in UI
| | POST /impression|click |
| |<---------------------------|

Prerequisites​

1. Enable the channel for your workspace​

In-app messaging must be turned on per workspace:

  1. Go to Settings → Messaging defaults (or edit the workspace under Workspaces).
  2. Enable In-app (allow_in_app).

If the channel is disabled, authors cannot select in-app when creating broadcasts or campaigns, and the broadcast detail page will warn that the channel is turned off.

2. Integrate the SDK​

Your mobile or web app must:

  • Identify users with the same person_id Conviso uses for campaigns and broadcasts.
  • Call the Data Gateway in-app endpoints (see SDK integration below).
  • Implement UI for each render_type you plan to use (modal, banner, inline, inbox_card).

The Flutter SDK (open_cdp_flutter_sdk) implements these routes; see In-app messaging (Flutter SDK) for OpenCDPConfig, CDPInAppManager, and the reference example/ UI. That is separate from Customer.io inAppConfig, which only applies when dual-writing to Customer.io.

3. No third-party provider required​

Unlike email (SendGrid) or SMS (Twilio/Termii), in-app messages are delivered entirely through your app and the Conviso Data Gateway. There is no external messaging provider to configure.


1. Sending via Broadcasts​

Use a broadcast when you want to reach a defined audience once (immediately or on a schedule).

  1. Navigate to Broadcasts → Create Broadcast.
  2. Complete Recipients, Goal, and Subscription preference as usual.
  3. On the Content step, select In-app as the channel.
  4. Click to open the In-app editor and design your message.
  5. Review and Send or Schedule.

Analytics for in-app broadcasts appear on the broadcast Overview and Analytics tabs (sent, shown, clicked, failed). See Analytics.

(For recipient targeting, goals, and scheduling, see the Broadcasts guide.)


2. Sending via Campaigns​

Use a campaign when in-app messages should be part of an automated journey.

  1. Create or edit a Campaign and open the Workflow builder.
  2. Click + to add a step and choose In-app from the messaging channels.
  3. Configure Render type and Persistence in the side panel, then click Add in-app content to open the full editor.
  4. Save the workflow and Start the campaign.

When a person reaches the in-app step, Conviso queues a delivery. The message is shown the next time your SDK syncs while the person is on an eligible screen.

(For triggers, delays, and branches, see Create a Campaign.)


The In-App Editor​

The same editor is used for broadcasts and campaign steps. It is split into a content area (title, body, image, CTA) and delivery rules (priority, expiry, page targeting, optional quiet hours, persistence).

Render type​

Choose how your app should present the message:

Render typeTypical use
modalFull-screen or centered overlay; best for announcements and onboarding
bannerTop or bottom strip; good for non-blocking tips
inlineEmbedded in a scrollable screen
inbox_cardMessage center / inbox list item

Your SDK must implement each render type you select. Conviso returns render_type on sync; rendering is your responsibility.

Content fields​

  • Title — Headline (optional). Supports Liquid personalization.
  • Body — Main copy. Supports Liquid.
  • Image URL — Optional banner or hero image (http/https). Can be templated with Liquid.
  • Call to action (CTA) — One primary button:
    • Label — Button text.
    • Action — deep_link, dismiss, or custom.
    • Value — Deep link URL or custom payload (for deep_link / custom).

When the user taps the CTA, your SDK should call the click tracking endpoint and handle navigation locally (e.g. open myapp://path for deep_link).

Priority​

When several messages are eligible at once, Conviso returns them sorted by priority (highest first), then by queue time. The dashboard exposes four presets:

LabelStored value
Low10
Normal50
High75
Critical100

Your SDK should render messages in the order returned by /sync without re-sorting.

Expiry​

Optionally set Expires after (minutes, hours, or days). After expiry, the delivery is no longer returned on sync. Choose Never expires if the message should remain eligible until shown, clicked, dismissed, or cleaned up by retention rules.

Page rules (screen targeting)​

Restrict where a message may appear:

  • Include screens — Multi-select of screen identifiers (creatable). Choose All screens to allow every screen (persisted as an empty include list), or pick specific screen names. Names must match what the SDK sends on /sync?screen=.
  • Exclude screens — Screens where the message must not appear. Exclude is always available, including when Include is All screens (meaning “everywhere except…”). Exclude always wins when both match.

Example: Include All + Exclude checkout → show everywhere except checkout. Include home, checkout — the SDK calls /sync?person_id=...&screen=home on the home route.

Screen naming (all platforms)​

Screen identity is the string your SDK sends as properties.screen on screen_view (and on /sync). Prefer short, stable ids:

ShapeExamples
Path-like/profile, /checkout/payment
Segmentsprofile.personal_details, Profile/PersonalDetails
Short labelsHome, Checkout
PlatformAuto-track defaultHow to set a readable nameSkipped
FlutterRouteSettings.name when setNamed routes / GoRouter paths; pass RouteSettings(name: '...') on dialogs and sheetsUnnamed routes and framework toString() dumps
AndroidActivity title if set, else Activity simple nameCall OpenCDP.trackScreenView(title) (e.g. from a NavController destination listener)Empty / anonymous / technical dumps
iOStitle / navigationItem.title, else cleaned VC type nameCall trackScreenView(title:) on appear (SwiftUI / coordinators)System/container VCs (UINavigationController, etc.)

Avoid: framework class dumps, anonymous route toString(), empty strings. Technical dumps are rejected by the catalog and never appear in Include / Exclude.

Screen catalog (how names appear in the editor)​

Conviso builds a workspace screen catalog from SDK screen_view events:

  1. The app navigates (or calls trackScreenView) with autoTrackScreens / manual tracking.
  2. The SDK posts eventName: "screen_view" with properties.screen to the Data Gateway.
  3. The gateway records that name into the catalog only (RECORD_SCREEN) — it does not write screen_view to analytics / BigQuery or fire campaign triggers.
  4. The in-app editor loads the catalog via Product API GET /screens for Include / Exclude pickers.

Dedup: Each distinct screen string is stored once per workspace (exact match after trim, case-sensitive). Repeated views only update last_seen.

Cold start: Until devices report screen views after this is enabled, the list may be empty. You can still type custom names or wildcards (e.g. checkout/*) in the creatable multi-select.

Name quality: Catalog entries are whatever the app sends (Home vs home vs /home are different). Keep navigation names stable so page rules match sync.

Screen names are defined by your app; keep them consistent between your navigation layer, setCurrentScreen / sync, and the Conviso editor.

Persistence and frequency​

Control how often a message can be shown:

SettingDescription
One timeAfter the first successful impression, the delivery is marked shown and typically not shown again.
Persistent until dismissedKeeps re-eligible until the user dismisses it (subject to max impressions / min interval).
Max impressionsCap total times the message can be shown per person.
Min intervalMinimum time between impressions (minutes, hours, or days).

These settings are available in the in-app editor for broadcasts and in the campaign workflow drawer + editor for campaigns.

Test message​

From the editor, use Send test to queue a message to a specific person (by profile ID). This uses the Product API test endpoint and is useful for QA before launching a broadcast or campaign.


Personalization with Liquid​

Title, body, image URL, and CTA fields support Liquid syntax, same as email and push.

Examples:

Hi {{ person.first_name | default: "there" }}, welcome back!
Your balance is {{ person.balance | default: 0 }}

Use the attribute sidebar in the editor to insert person.* or event.* placeholders. At send time, Conviso renders Liquid per recipient; the resolved content is stored on the delivery when pre-rendered.


The Conviso Flutter SDK (open_cdp_flutter_sdk) implements this contract end-to-end: OpenCDPConfig.enableInAppMessages, OpenCDPSDK.instance.inApp (CDPInAppManager), SSE on /stream, and helpers for impression / click / dismiss. See the dedicated guide: In-app messaging (Flutter SDK).

SDK integration​

All in-app Data Gateway routes live under:

/data-gateway/v1/in-app/messages

Authenticate with your workspace API key (same key used for identify, track, and device registration).

Fetch eligible messages (sync)​

GET /data-gateway/v1/in-app/messages/sync

Query parameters

ParameterRequiredDescription
person_idYesExternal person identifier (same as identify)
screenNoCurrent screen/route name for page rules
timezoneNoIANA timezone (e.g. America/New_York) for quiet-hours resolution
tz_offset_minutesNoDevice UTC offset in minutes (fallback for quiet hours)
limitNoMax messages to return (1–50, default 10)

Response — array of messages, each including:

FieldDescription
delivery_idUse for impression / click / dismiss calls
message_idShared definition id
render_typemodal, banner, inline, or inbox_card
priorityNumeric priority
content.title, content.body, content.image_urlRendered content
ctasArray of { id, label, action, value? }
expires_atISO timestamp or null
persistence{ mode, max_impressions_total?, min_interval_seconds? }

Call /sync on app launch, after navigation (screen change), after receiving an SSE sync event, and on a sensible background poll interval.

Realtime stream (optional)​

GET /data-gateway/v1/in-app/messages/stream?person_id={person_id}

Returns text/event-stream:

  • : connected — connection established
  • : hb — heartbeat every ~20s
  • event: sync with data: {"delivery_id":"...","ts":"..."} — new delivery queued; call /sync immediately

Track interactions​

Report events after the user sees or interacts with a message:

POST /data-gateway/v1/in-app/messages/{delivery_id}/impression
POST /data-gateway/v1/in-app/messages/{delivery_id}/click
POST /data-gateway/v1/in-app/messages/{delivery_id}/dismiss

Body (JSON)

FieldRequiredDescription
person_idYesSame identifier used on sync
screenNoScreen where the event occurred
platformNoe.g. ios, android, web
app_versionNoApp version string
action_idNoCTA id (for click events)
reasonNoDismiss reason (for dismiss events)
tsNoISO timestamp
EndpointWhen to call
impressionMessage became visible to the user (shown)
clickUser tapped a CTA
dismissUser closed the message without the primary CTA

Impressions update delivery status to shown and feed delivered / shown analytics. Clicks update clicked metrics. Dismissals mark the delivery dismissed.

SDK implementation checklist​

  • Identify users before syncing in-app messages.
  • Pass the current screen on every /sync call when using page rules.
  • Render messages in sync response order; enforce one modal at a time in UI if needed.
  • Call impression only after the message is actually visible.
  • Handle CTA deep_link / custom actions in app code; report click with action_id.
  • Open the SSE stream while the app is foregrounded (optional but recommended).
  • Respect expires_at and persistence client-side as a safety net (server also filters on sync).

Analytics​

Campaigns​

Campaign analytics aggregate in-app alongside email, SMS, and push in BigQuery-backed reports. Metrics include sent, delivered (shown), clicked, and failed (including suppressed/failed delivery events rolled into failed counts).

Filter by the In-app channel on the campaign Analytics tab.

Broadcasts​

Broadcast in-app analytics are stored in MongoDB (broadcast_analytics with channel in_app). The dashboard shows:

  • In-app sent — Messages queued for delivery
  • Shown — Successful impressions
  • Clicked — CTA taps
  • Failed — Delivery failures

Activity log​

In-app queue and interaction events also appear in per-person activity logs and message activity views, consistent with other channels.


Quiet hours​

In the In-app message editor, enable Quiet hours to block this message during a daily window. While a person is inside that window (in the selected timezone), /sync does not return the message; after the window ends, it becomes eligible again on the next sync (subject to expiry, page rules, and persistence).

Configure:

  • Timezone for quiet hours
    • Workspace default — Uses your organisation’s default timezone from settings.
    • User’s device — Uses the timezone or UTC offset the SDK sends on /sync (timezone or tz_offset_minutes). Pass one of these when you choose this mode so evaluation is accurate.
  • Start and End — 24-hour clock times (HH:MM). Overnight windows are supported (for example 22:00 to 08:00 means quiet from 10pm through 8am).

If quiet hours are disabled, the message is only gated by page rules, expiry, and persistence.


Best practices​

  • Screen naming — Use stable, documented screen ids (checkout, not CheckoutScreen_v2) so page rules stay maintainable.
  • One modal at a time — Even when sync returns multiple messages, show one modal and queue the rest.
  • Don't skip impression tracking — Analytics and persistence depend on accurate impression calls.
  • Test on real devices — Use Send test in the editor and verify deep links on iOS and Android.
  • Match priority to urgency — Reserve Critical for true interruptions; overuse reduces trust.
  • Keep copy short — Modals and banners have limited space; put detail behind a deep link CTA.