> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# The in-app channel model: pull delivery, two render classes, segment targeting, and engagement receipts

> How Orbit's owned, zero-carrier-cost in-app channel works — why delivery is a pull rendered by your web or React Native SDK rather than a push through a gateway, the persistent content card vs transient in-app message render classes, segment-scoped eligibility, and why engagement events replace carrier receipts.

# 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](/guides/in-app-channel-messages) 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](/concepts/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](/guides/website-personalization-slots) 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](/guides/delivery-log) 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:

* **Web** — `OrbitInApp` 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 Native** — `OrbitInAppClient` 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](/concepts/fallback-and-cascade-planes) 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

* [In-app channel guide](/guides/in-app-channel-messages) — the operational how-to: enable, author, target, render, and read engagement.
* [Cross-channel fallback](/concepts/cross-channel-fallback) — pairing in-app with carrier hops.
* [Push channel model](/concepts/push-channel-model) — the device-registry push counterpart.
* [Website personalization](/guides/website-personalization-slots) — the same segment signal used for targeting.
