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/feedand renders it with its own components. - Engagement (impressions, clicks, dismissals) is reported back by the SDK in one lightweight
POST /sdk/in-app/eventscall — the same traffic your app already pays for.
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 throughPUT /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 carrysegmentLabels, 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 carrybuttonId, so a two-button message splits cleanly.dismiss— the visitor closed the surface; the id also joins the per-device dismissed list.
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:- Web —
OrbitInAppfrom 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 Native —
OrbitInAppClientfrom the React Native SDK. Same feed and event model; returns raw{ cards, messages }data for your own components and reports events the same way.
identify the visitor) and a per-device dismissed list, so a dismissed surface never flashes back on the next fetch.
Section 6 — Coexistence with carrier channels: the worked cascade pairing
In the Messages hub, in-app composes with the carrier channels rather than competing with them. The canonical pairing is a cascade fallback chain that prefers the free in-app surface and escalates the rest to a carrier leg. The cascade fallback guide owns the chain syntax; this section lays out the model-level pairing that makes in-app first the right default. The chain, numbered:- In-app first — the zero-carrier-cost hop. The SDK’s next
GET /sdk/in-app/feedpull renders the surface for every reachable visitor. That whole audience costs nothing on this hop — Section 1’s cost model applies: no per-send charge, no gateway leg. - Evaluate the check-in window. Give the SDK audience a bounded window (hours, not minutes) to fetch and render. Reachable visitors consume the surface; unreached ones move down the chain.
- Escalate to a carrier backstop. Visitors with no active in-app surface — no web/React Native SDK running for subsequent
identifypulls — are handed to SMS or email, carrying the same offer. Only this hop pays a carrier leg, and it pays it only for the audience in-app genuinely cannot reach.
GET /sdk/in-app/feed, not at send time. Each visitor’s pull reports “you do (or do not) have an active in-app surface,” and the chain reads that signal per-pull. That is what separates the in-app fallback pairing from the carrier-to-carrier chains on the cross-channel fallback page: the advance trigger fires on reachability, not on a delivery receipt, because an in-app hop has no DLR to wait on.
Worked example — segment-targeted offer with an SMS backstop. You target segmentLabels: ["vip_welcome"] on a content card. A visitor whose resolved primary segment (contact_scores.segment_label) matches, and whose browser/app runs the SDK, renders the card on their next feed fetch at zero carrier cost. A second matching visitor — same segment, but no active SDK surface — never produces a fetch, so the chain escalates their vip_welcome offer to SMS after the check-in window. One audience, one offer, two channel hops, and the carrier leg pays only for the second visitor. Broadening the chain to email as a third hop is a one-line change on the same model.
Reach is why the backstop exists, and it is not something to paper over: the in-app channel can only reach a visitor who runs your web or React Native SDK. For anything with a hard reach guarantee, pair every in-app hop with a carrier fallback — never author in-app as the sole hop when the message must land.
Where to go next
- In-app channel guide — the operational how-to: enable, author, target, render, and read engagement.
- Cross-channel fallback — pairing in-app with carrier hops.
- Push channel model — the device-registry push counterpart.
- Website personalization — the same segment signal used for targeting.