In-App Experiences
In-app messages reach users inside your product: a durable inbox delivers them even when the user was offline, and authored banner and modal formats render on web with no code change in your app. This guide covers authoring, display rules, the prebuilt web components, and engagement tracking.
Availability: every plan, including Free (since 2026-07-11). The retention caps — 30-day message expiry and a 100-message inbox per subscriber — apply on all tiers.
Formats
Author rendered experiences in the In-App studio (Messaging › In-App): pick a format, add images and buttons, target an audience, schedule a live window, and publish. Compose still delivers headless title/body sends to the inbox when an in-app channel is targeted.
| Format | What happens |
|---|---|
| Headless (default) | Your app receives {title, body, data} through the SDK and renders it itself — the original behavior. |
| Banner | A dismissible strip rendered at the top of the page by the browser SDK. |
| Modal | A centered dialog with optional image and call-to-action buttons. |
| Card | A persistent floating card in the corner — stays until dismissed, no auto-hide. |
| Fullscreen | A viewport takeover with a hero image, title, body, and CTAs. |
| Inbox only | Delivered silently to the message inbox — no on-screen surface. |
SDK versions that predate a format degrade gracefully: mobile SDKs render unknown formats as modals; older browser SDKs suppress them (the message stays in the inbox).
Banners and modals support an optional image URL and a CTA button (label + link). Clicking the CTA opens the link and records an inapp_click with the button's id.
Quiet hours: an inbox only send that targets only in-app channels is exempt from quiet-hours deferral — it interrupts nobody. Frequency caps still apply. Any other format, or a send that also targets push/email, respects quiet hours normally.
Display rules
Display rules are evaluated client-side by the SDK — it knows session state; the server does not.
- Show: Immediately (on receipt), At next session start, or After an event. A "session start" message that arrives while the user is mid-session is held; the durable inbox delivers it when the SDK next connects. An "after an event" message surfaces when your app tracks the named behavioral event — every SDK evaluates it locally against its own
track()calls (browserHober.track(), iOSHober.track(), AndroidHober.track(), React NativeuseHober().track()), so the message appears immediately, no server round-trip. Messages whose event never fires stay safely in the inbox. - Frequency: Once (ever, per message, per browser) or Once per session.
- Priority (1–10, default 5): when several messages are pending, the highest-priority one displays first; the rest queue.
Rendering on web
One call replaces the headless setup — banners and modals then display automatically:
import { createRenderedClient } from '@hoberhq/browser-sdk';
const { client } = createRenderedClient({
apiKey: 'pk_your_sdk_key',
subscriberID: currentUser.id,
});
client.connect();
The surfaces are plain DOM with hober-inapp-* class names, themeable without JavaScript:
:root {
--hober-inapp-bg: #1f2937;
--hober-inapp-fg: #f9fafb;
--hober-inapp-accent: #f59e0b;
}
Notification-center widget
Embed an inbox bell (unread badge, message list, mark-all-read, dismiss):
import { InAppClient, NotificationCenter } from '@hoberhq/browser-sdk';
const client = new InAppClient({ apiKey: 'pk_…', subscriberID: currentUser.id });
new NotificationCenter(client, { container: document.querySelector('#bell')! });
Rendering on mobile
Each mobile SDK ships the same display-rule engine and engagement semantics as the web renderer. Construct the in-app client with impressions disabled (the renderer reports them at display time) and attach the platform's renderer:
iOS (SwiftUI, presented in a passthrough window):
let client = HoberInAppClient(apiKey: key, subscriberID: userID, reportImpressions: false)
let renderer = HoberInAppRenderer(client: client)
client.connect()
Android renders through a surface port — the SDK owns the rules and engagement, your app (or the example app's ready-made implementation) owns the views, so surfaces match your design system:
val client = HoberInAppClient(apiKey, subscriberID, reportImpressions = false)
val renderer = InAppRenderer(client, surface = mySurface) // implements InAppSurfacePort
client.connect()
React Native — mount the surface once near the app root:
<HoberInAppSurface client={inAppClient} />
On all three, once frequency persists per install where the platform allows (UserDefaults on iOS; bring a SharedPreferences/AsyncStorage-backed ShownStore on Android and React Native — adapters are documented in the SDK sources).
Tracking events from mobile
All four SDKs now record behavioral events with the same semantics: batched to POST /v1/events (10 pending or 500ms debounce), anonymous until identifySubscriber, stitched to the subscriber afterwards. Tracked events feed segments, journey waits, and event-triggered in-app messages:
try Hober.track("cart_abandoned", properties: ["value": 129])
Hober.track("cart_abandoned", mapOf("value" to 129))
const { track } = useHober()
track('cart_abandoned', { value: 129 })
P1 buffering notes: buffers are in-memory (a killed app loses unflushed events), and the anonymous id is per-install on iOS/web but per-process on Android and per-runtime on React Native unless you inject a stable one.
Staying headless
Nothing changes for existing integrations: onMessage still delivers every message, now including the authored fields (format, imageUrl, actions, display) so you can render them your own way on any platform.
Engagement
The renderer and widget report engagement automatically through the interactions pipeline:
| Event | When |
|---|---|
inapp_impression | A banner/modal actually displays (not merely delivers) |
inapp_click | A CTA button is tapped (carries the action id) |
inapp_dismiss | The user closes the surface or dismisses from the inbox |
These events feed segments ("clicked an in-app message in the last 7 days") and analytics like any other behavioral event. Headless integrations report the same events via client.reportImpression, client.reportClick, and client.dismiss.
Journeys and analytics
In-app engagement flows through the same behavioral pipeline as every other event, so it composes with the rest of the platform without extra setup:
- Journeys: a Wait for event step can wait on in-app engagement. The event names are
notification.inapp_click,notification.inapp_impression, andnotification.inapp_dismiss— e.g. "send the in-app offer → wait up to 7 days fornotification.inapp_click→ branch to the converted path, else send a reminder". - Segments: target subscribers by in-app engagement like any behavioral event ("clicked an in-app message in the last 7 days").
- Funnels: campaign and journey funnels count in-app impressions as opens and in-app clicks as clicks automatically.
Retention
Inbox messages expire after 30 days, and each subscriber's inbox keeps the most recent 100 messages (oldest evicted). Read state is subscriber-scoped: marking a message read on one device syncs to all of them.