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.
How Placements Work
Section titled “How Placements Work”- You define slots in your UI — named positions where placements can appear
- The SDK evaluates targeting rules against the current user context
- If a rule matches, the SDK resolves a placement decision with content, surface template, and CTA path
- The slot component renders the decision using a built-in or custom template
Slot Configuration
Section titled “Slot Configuration”Every slot needs an id and optionally a list of accepted surfaceTemplateIds:
<Slot id="dashboard_banner" surfaceTemplateIds={['banner_placement', 'banner_announcement']}/>| Field | Type | Description |
|---|---|---|
id | string | Unique slot identifier (matches server-side placement rules) |
surfaceTemplateIds | string[] | Templates this slot accepts (filters available placements) |
Slot Types
Section titled “Slot Types”Fixed Slots
Section titled “Fixed Slots”Always-visible inline placements — buttons, cards, meters:
import { Slot } from "@revturbine/sdk";import { DemoApp } from "./DemoApp";
// Carol is on Starter with an active trial — the engine serves her an upgrade// button in this slot. Switch to user="user_eve" and it renders nothing.export default function App() { return ( <DemoApp user="user_carol"> <Slot id="nav_bar_right" surfaceTemplateIds={["button"]} /> </DemoApp> );}Opens a live, editable sandbox with the published SDK.
Fixed slots render nothing if no placement matches — your layout stays intact.
Access Gate Slots
Section titled “Access Gate Slots”Wrap premium content and show an upgrade prompt when access is denied:
import { Gate } from "@revturbine/sdk";import { DemoApp } from "./DemoApp";
// Dan lacks data_export, so the gate renders the configured upgrade placement// in place of the children. Switch to user="user_alice" to see the children.export default function App() { return ( <DemoApp user="user_dan"> <Gate id="editor_export_action" surfaceTemplateIds={["modal_overlay"]} can="data_export" > <button>Export Video</button> </Gate> </DemoApp> );}Opens a live, editable sandbox with the published SDK.
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 dashboard without a code deploy. deniedFallback is shown only when no placement matches.
Message Slots
Section titled “Message Slots”Page-level overlays triggered by targeting rules — toasts, modals, banners:
import { Slot } from "@revturbine/sdk";import { DemoApp } from "./DemoApp";
// A message slot renders nothing until a targeting rule fires. Alice's usage// trips the warning rule, so the engine serves her a banner here.export default function App() { return ( <DemoApp user="user_alice"> <Slot id="global_banner" surfaceTemplateIds={["banner_placement"]} /> </DemoApp> );}Opens a live, editable sandbox with the published SDK.
Message slots render nothing until a targeting rule triggers. Place them at the top level of your layout.
Placement Output
Section titled “Placement Output”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;}config_version lets you detect a stale Playbook; decision_id is the ID to quote in debugging and analytics; present_upsell tells you whether the placement carries an upgrade or expansion offer.
Placement Lifecycle
Section titled “Placement Lifecycle”Impression → Interaction → Outcome
Section titled “Impression → Interaction → Outcome”┌─────────┐ ┌──────────┐ ┌──────────┐│ Resolve │ ──► │ Visible │ ──► │ Interact │└─────────┘ └──────────┘ └──────────┘ │ │ │ ┌─────┴─────┐ │ │ dismiss │ │ │ snooze │ │ │ ctaClick │ │ │ ctaComplete│ │ └───────────┘ │ ▼ (auto-tracked impression)Interaction Methods
Section titled “Interaction Methods”const { dismiss, snooze, ctaClick, ctaComplete } = usePlacement({ placement: { name: 'hero_banner' } });
// Dismiss — suppresses for default cooldownawait dismiss();
// Dismiss with custom cooldown (30 minutes)await dismiss(30 * 60 * 1000);
// Snooze — temporarily suppress (1 hour)await snooze(3600);
// Record CTA clickawait ctaClick();
// Record CTA completion (e.g., checkout finished)await ctaComplete();Each interaction is automatically tracked as a treatment interaction event.
Cap Policies
Section titled “Cap Policies”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.
Placement Chaining
Section titled “Placement Chaining”A CTA can point to another placement via cta_path.placement_handle:
import { parseUiPath } from '@revturbine/sdk';
// First placement decides: show upgrade bannerconst 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 placementconst path = banner?.cta_path ? parseUiPath(banner.cta_path) : undefined;const followUp = path?.placement_handle ? await sdk.getPlacement({ placementHandle: path.placement_handle }) : null;CTA actions
Section titled “CTA actions”Every placement carries a cta_path — the action its button triggers. Its type is the action_type of a content_ui_paths entry in your Playbook: one of the built-in types (open_checkout, view_plans, book_demo, contact_sales, complete_onboarding, invite_teammate, refer_friend, verify_work_email, update_payment_method, enable_auto_renewal, manage_subscription, switch_billing_period, extend_trial, open_rt_placement, dismiss, snooze) or a name you define — a custom action flows through under its own name. The starter Playbook uses navigate_to_plans and open_checkout_modal.
The SDK never navigates, opens checkout, or calls your backend on its own. On click it looks up a resolver for cta_path.type and calls it with the authored cta_path record (plan_handle, promotion_id, placement_handle, url, plus any custom params). If no resolver is registered, it tracks the click and calls the slot’s onCtaClick fallback — nothing else happens. snooze has a built-in resolver; everything else needs one from you.
Register resolvers once, at init, keyed by action type:
<RevTurbineProvider options={{ // ... uiPathResolvers: { navigate_to_plans: () => router.push('/pricing'), open_checkout_modal: (path) => openCheckout(path.plan_handle, path.promotion_id), open_rt_placement: async (path) => { const next = await rt.getPlacement({ placementHandle: path.placement_handle }); if (next) showPlacement(next); }, }, }}> <YourApp /></RevTurbineProvider>At init the SDK checks that every action_type in content_ui_paths has a resolver and warns about gaps; rt.validateUiPathResolvers({ throwOnMissing: true }) turns the warning into an error, which is useful in tests. Actions that must run on your server — extend_trial, for example — go in serverActions instead: see Server-owned CTA actions.
Using the Hook
Section titled “Using the Hook”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});Live examples
Section titled “Live examples”One scenario runs here; the full gallery — every built-in slot type, editable, switching demo users — lives on the Interactive Playground.
Upgrade Button — an always-visible CTA in a nav slot.
import { Slot } from "@revturbine/sdk";
export function Example() { return ( <Slot id="nav_bar_right" surfaceTemplateIds={["button"]} /> );}More on the Playground: Plans & Pricing Card · Quota Meter · Annual Nudge Banner · Usage Warning Banner · Usage Exhausted Modal · Trial Welcome Toast.
Route surface
Section titled “Route surface”Every slot stamps the app route it rendered on onto its lifecycle events, so
discovery groups the same slot across /projects/42 and /projects/43
instead of treating each URL as a separate surface. See
Route surface in the Placement
Cookbook for the <RevTurbineRoute> boundary, the Next.js recipe, and the
pathname fallback.
Next Steps
Section titled “Next Steps”- Component Gallery — interactive demos of every built-in slot component
- Custom Slot Types — register your own slot components
- Events & Analytics — track impressions and interactions
- Interactive Playground — try all slot types live