Skip to content

Placements

Placements are the SDK’s core output — they’re the decisions about what content to render in which slot, for which user, at what moment. This guide covers placement configuration, resolution, lifecycle, and chaining.

  1. You define slots in your UI — named positions where placements can appear
  2. The SDK evaluates targeting rules against the current user context
  3. If a rule matches, the SDK resolves a placement decision with content, surface template, and CTA path
  4. The slot component renders the decision using a built-in or custom template

Every slot needs an id and optionally a list of accepted surfaceTemplateIds:

<Slot
id="dashboard_banner"
surfaceTemplateIds={['banner_placement', 'banner_announcement']}
/>
FieldTypeDescription
idstringUnique slot identifier (matches server-side placement rules)
surfaceTemplateIdsstring[]Templates this slot accepts (filters available placements)

Always-visible inline placements — buttons, cards, meters:

Fixed slots render nothing if no placement matches — your layout stays intact.

Try it live → Upgrade Button

Wrap premium content and show an upgrade prompt when access is denied:

If the entitlement is granted, children render normally. If denied, the gate renders an upgrade placement in their place.

That denial UI is itself a placement, not hardcoded markup: the decision engine picks the winning placement for the gate (scoped by surfaceTemplateIds) and renders its content — modal or banner, headline, offer, and CTA. So what a denied user sees is controlled by your monetization config and can be changed, targeted, or A/B-tested in the studio without a code deploy. deniedFallback is shown only when no placement matches.

Try it live → Data Export Gate

Page-level overlays triggered by targeting rules — toasts, modals, banners:

Message slots render nothing until a targeting rule triggers. Place them at the top level of your layout.

Try it live → Usage Warning Banner

When a placement resolves, the decision contains:

interface PlacementOutput {
output_id: string;
category: string;
surface: {
type: RevTurbineComponentType;
template?: string;
slot_id?: string;
};
content: Record<string, unknown>; // toast duration is in seconds
cta_path?: Record<string, unknown>;
promotion?: Record<string, unknown>;
rule_id: string;
decision_id: string;
config_version: string;
present_upsell: boolean;
}
┌─────────┐ ┌──────────┐ ┌──────────┐
│ Resolve │ ──► │ Visible │ ──► │ Interact │
└─────────┘ └──────────┘ └──────────┘
│ │
│ ┌─────┴─────┐
│ │ dismiss │
│ │ snooze │
│ │ ctaClick │
│ │ ctaComplete│
│ └───────────┘
(auto-tracked impression)
const { dismiss, snooze, ctaClick, ctaComplete } = usePlacement({ placement: { name: 'hero_banner' } });
// Dismiss — suppresses for default cooldown
await dismiss();
// Dismiss with custom cooldown (30 minutes)
await dismiss(30 * 60 * 1000);
// Snooze — temporarily suppress (1 hour)
await snooze(3600);
// Record CTA click
await ctaClick();
// Record CTA completion (e.g., checkout finished)
await ctaComplete();

Each interaction is automatically tracked as a treatment interaction event.

Placements can author cap policies that limit how often they appear:

const capPolicy = {
caps: {
max_per_period: { count: 3, period: 'session' },
cooldown_days: 1,
},
};

Authoring a cap in the Playbook enables client-side cap enforcement for that placement. The decision API returns visible: false with a cap_exceeded reason when capped; the convenience getPlacement() API returns null.

A CTA can point to another placement via cta_path.placement_handle:

import { parseUiPath } from '@revturbine/sdk';
// First placement decides: show upgrade banner
const banner = await sdk.getPlacement({ slotId: 'dashboard_banner' });
// banner.cta_path = { type: 'open_rt_placement', placement_handle: 'checkout_offer' }
// On CTA click, resolve the follow-up placement
const path = banner?.cta_path ? parseUiPath(banner.cta_path) : undefined;
const followUp = path?.placement_handle
? await sdk.getPlacement({ placementHandle: path.placement_handle })
: null;

Map CTA actions to application navigation:

<RevTurbineProvider
options={{
// ...
uiPathResolvers: {
open_pricing: () => router.push('/pricing'),
upgrade: (uiPath) => {
stripe.redirectToCheckout({ priceId: uiPath.plan_handle });
},
start_trial: () => router.push('/trial/start'),
},
}}
>
<YourApp />
</RevTurbineProvider>

When a user clicks a CTA, the SDK calls the matching resolver.

For custom rendering, use usePlacement directly:

const {
visible,
content,
decision,
isLoading,
error,
refresh,
dismiss,
ctaClick,
} = usePlacement({
surfaceSlot: {
id: 'dashboard_banner',
surfaceTemplateIds: ['banner_placement'],
},
ttlMs: 300_000, // Cache for 5 minutes
autoLoad: true, // Load on mount
});

Every built-in slot type, running live in local_only mode — edit the code and switch demo users to see the engine respond.

Upgrade Button — an always-visible CTA in a nav slot.

Plans & Pricing Card — an inline pricing/plans card.

Quota Meter — a usage meter with progress bar and counter.

Annual Nudge Banner — an annual-plan nudge banner.

Usage Warning Banner — a “running low” usage banner.

Usage Exhausted Modal — a limit-reached modal.

Trial Welcome Toast — a trial-welcome toast.