Skip to main content

The in-app channel model

The in-app channel is Orbit’s owned engagement surface: content cards (a persistent, feed-style list) and in-app messages (transient modal, slide-up, fullscreen, or HTML overlays) that render inside your own website or app. It sits alongside SMS, WhatsApp, and email in the Messages hub, and it is the one channel that asks nothing of a carrier — this page explains the model behind it. The in-app channel guide is the operational how-to with copy-pasteable requests; the schema details below are deliberately not duplicated here.

Section 1 — Why in-app is zero-carrier-cost

Every carrier channel pays a per-message leg: an SMS exits through a gateway, a WhatsApp template exits through Meta, an email exits through a provider. The in-app channel has no carrier leg at all, because delivery on this channel is a pull, not a push:
  • Your app fetches the visitor’s eligible feed from GET /sdk/in-app/feed and renders it with its own components.
  • Engagement (impressions, clicks, dismissals) is reported back by the SDK in one lightweight POST /sdk/in-app/events call — the same traffic your app already pays for.
Because eligibility is computed at fetch time, not send time, there is no per-send cost, no sender ID to register, and no throughput ceiling set outside your own infrastructure. The trade-off is reach: the channel can only show content to a visitor who runs your web or React Native SDK. Pair it with a carrier fallback (see cross-channel fallback) when a hard reach guarantee matters. This is the owned-channel parity that other engagement vendors sell as a separate product line; here it ships with the rest of Messages, one master toggle per tenant.

Section 2 — Two render classes

The channel splits into two render classes with deliberately different lifetimes. Both are authored on the Messages → In-app console page (or through PUT /api/v1/messages/in-app), and both travel over the same feed. Content cards — persistent, feed-style. A card is a durable surface that lives in the visitor’s feed until it expires or is dismissed. Three families exist: classic (title-led), banner (image-only, the whole card is the tap target), and captioned_image (image plus title). pinned: true floats a card to the top of the feed; dismissible controls whether the visitor can remove it. Cards are the “inbox that never pushes” surface — a promotion, an announcement, an order status that should stay visible across sessions. In-app messages — transient overlay. A message renders once as a modal, slideup, fullscreen, or html overlay and then leaves the feed. Buttons (capped at five; actions close, url, or deeplink) carry the call to action, and a numeric priority decides which message wins when several are eligible — the SDK shows one at a time. Messages are the “interrupt politely” surface — a welcome back nudge, a VIP preview, a one-time announcement. Both classes share expiresAt (ISO-8601), after which a surface drops out of the feed server-side, so a time-boxed campaign never needs a cleanup job. The authored set is capped at 100 cards and 100 messages per tenant — a real feed, not a firehose.

Section 3 — Targeting model

What a visitor sees is never a broadcast; it is decided per fetch by stacking eligibility checks. The primary targeting lever is segment-scoped delivery with the same Contacts/CDP vocabulary the carrier channels use: any card or message may carry segmentLabels, and a labeled surface is served only to a visitor whose resolved primary segment (contact_scores.segment_label, the same signal website personalization uses) is on the list. An unlabeled surface is global. Two more levers stack on top: expiry filters time-boxed surfaces on every fetch, and per-device suppression keeps a dismissed id out of that device’s feed. Because the visitor’s segment assignment is resolved on each GET /sdk/in-app/feed, a change to either side — a new label on a surface, or a fresh identify on the visitor — takes effect on the next fetch with no separate publish step.

Section 4 — Engagement events

Carrier channels close the delivery loop with DLRs — receipts handed back by each carrier. The in-app channel has no carriers to hand receipts back, so engagement is the receipt: the SDK reports three verbs per rendered surface, tracked client-side and aggregated server-side.
  • impression — the surface was rendered to this visitor (an “open” for a message, a “view” for a card).
  • click — the visitor tapped the surface or one of its buttons; button-level events carry buttonId, so a two-button message splits cleanly.
  • dismiss — the visitor closed the surface; the id also joins the per-device dismissed list.
These events land in the same tenant SDK-event stream the rest of the analytics stack reads, so open rate (impressions per unique visitor), click-through (clicks per impression), and per-surface drop-off (dismissals) all compute from one place — pull them through the same delivery logs and Insights dashboards you use for carrier channels. Because the channel is pull-based, an impression event is a certainty signal rather than a carrier’s best-effort receipt: the surface rendered on the device, or the event never arrives.

Section 5 — SDK integration surface

Two shipped SDK surfaces render the feed, and they share one wire model so a surface authored once renders identically in both:
  • WebOrbitInApp from the web SDK. Fetches the visitor’s feed with your public API key, renders content cards into a container element you own (renderCards), and shows the highest-priority eligible message as an overlay (showMessage). Rendered nodes are tagged (orbit-inapp-card orbit-inapp-card--<type>) so your CSS takes over.
  • React NativeOrbitInAppClient from the React Native SDK. Same feed and event model; returns raw { cards, messages } data for your own components and reports events the same way.
Both SDKs keep a sticky anonymous id per device (until you identify the visitor) and a per-device dismissed list, so a dismissed surface never flashes back on the next fetch. Coexistence with carrier channels. In the Messages hub, in-app composes with the carrier channels rather than competing with them: cascade fallback chains can prefer the free in-app surface for reachable visitors and escalate to SMS or email only for the rest — the cheap channel primary, the carrier leg as backstop. That is the same pairing discipline described in the guide’s worked examples, at the model level.

Where to go next