Skip to content

Headless API

The headless API provides three controllers and a factory function:

| Export | Purpose | |---|---| | initRevTurbine() | Async factory — creates an initialized SdkSession | | SdkSession | Session wrapper with user context, placement, and entitlement APIs | | PlacementController | Register → decide → impression-track → interact lifecycle | | EntitlementGate | Check → auto-gate pipeline for feature access |

import {
initRevTurbine,
PlacementController,
EntitlementGate,
} from '@revturbine/sdk/headless';
import { initRevTurbine } from '@revturbine/sdk/headless';
const session = await initRevTurbine({
tenantId: 'tenant_abc',
apiKey: 'rt_live_xxx',
endpoint: 'https://api.revturbine.io',
user: { id: 'user_123', plan: { id: 'pro' } },
});
// Placement
const banner = session.placement({ surfaceSlot: { id: 'upsell_banner' } });
await banner.load();
if (banner.visible) {
console.log('Show banner:', banner.content);
}
// Entitlement
const gate = session.entitlement({ handle: 'brand_kit', autoGate: true });
await gate.check();
if (gate.denied) {
console.log('Show upgrade gate:', gate.gatedPlacement);
}

Creates a fully-initialized session. Handles SDK initialization, user identification, theme resolution, and optional placement bootstrapping.

const session = await initRevTurbine({
tenantId: 'tenant_abc',
apiKey: 'rt_live_xxx',
endpoint: 'https://api.revturbine.io',
user: { id: 'user_123', plan: { id: 'pro' } },
runtimeMode: 'revturbine_server',
bootstrapPlacements: [
{ placement: { name: 'pricing_banner' } },
],
});

Extends RevTurbineInitInputOptions with:

| Option | Type | Description | |---|---|---| | bootstrapPlacements | Array<{placement, userId?, contextMode?, overrides?, traits?, ttlMs?}> | Placements to preload on creation |

All standard init options apply: tenantId, apiKey, endpoint, runtimeMode, user, localRuntime, provider, providerFallbacks, etc.

The session returned by initRevTurbine(). Provides the full imperative API.

session.identify('user_456', { plan: { id: 'enterprise' } });
session.setUserContext({ personalization: { company: 'Acme' } });
const ctx = session.getUserContext();
session.resetIdentity();
session.updateUsage({ api_calls: 150 });
const fullCtx = await session.fetchUserContext('user_456');
const trial = await session.getTrialStatus();
const usage = session.getUsage();
const ctrl = session.placement({ surfaceSlot: { id: 'upsell_banner' } });
const gate = session.entitlement({ handle: 'brand_kit', autoGate: true });
const decision = await session.getPlacementBySlotId('upsell_banner');
const result = await session.checkEntitlement('brand_kit');
await session.trackEvent('feature_used', { feature: 'export_csv' });

Manages the full placement lifecycle: register → decide → impression-track → interact.

const ctrl = session.placement({
surfaceSlot: { id: 'pricing_banner' },
});
await ctrl.load();
if (ctrl.visible) {
renderBanner(ctrl.content);
}

| Option | Type | Default | Description | |---|---|---|---| | surfaceSlot | {id, name?} | — | Canonical surface slot (preferred) | | placement | RevTurbinePlacementConfig | — | Alternative: placement config | | userId | string | SDK user | Override target user | | contextMode | RevTurbineContextMode | 'auto' | Context resolution mode | | overrides | RevTurbinePlacementDecisionOverrides | — | Override segment/plan/usage for testing | | traits | Record<string, string \| number \| boolean> | — | Custom traits for decision request | | ttlMs | number | — | Decision cache TTL | | autoTrackImpression | boolean | true | Auto-record impression on visible decision |

ctrl.visible; // boolean — is the placement visible?
ctrl.content; // resolved content or null
ctrl.decision; // full decision object or null
ctrl.placementId; // registered placement ID
ctrl.state; // full PlacementControllerState snapshot
await ctrl.dismiss(); // dismiss with 24h cooldown
await ctrl.dismiss(7200_000); // custom cooldown (ms)
await ctrl.snooze(); // snooze for 1 hour
await ctrl.snooze(7200); // custom snooze (seconds)
await ctrl.ctaClick('upgrade'); // CTA click
await ctrl.ctaComplete('upgrade'); // CTA completed (hides placement)
await ctrl.refresh(); // re-fetch decision
ctrl.reset(); // reset all state
const unsub = ctrl.onChange(() => {
console.log('State changed:', ctrl.state);
updateUI(ctrl.state);
});
unsub(); // stop listening

Checks feature access and optionally resolves a gated placement for denied users.

const gate = session.entitlement({
handle: 'brand_kit',
autoGate: true,
});
await gate.check();
if (gate.allowed) {
enableFeature();
} else if (gate.denied && gate.gatedPlacement) {
showUpgradeModal(gate.gatedPlacement);
}

| Option | Type | Default | Description | |---|---|---|---| | handle | string | — | Entitlement handle (e.g. 'brand_kit') | | context | RevTurbineEntitlementContext | — | Optional usage/tier context | | autoGate | boolean | false | Auto-resolve gated placement when denied | | gatePlacementRequest | Omit<...> | — | Custom placement request for auto-gate |

gate.allowed; // boolean
gate.limited; // boolean — usage partially exhausted
gate.denied; // boolean
gate.result; // raw EntitlementResult or null
gate.gatedPlacement; // PlacementOutput (when denied + autoGate)
gate.state; // full EntitlementGateState snapshot
const unsub = gate.onChange(() => {
updateAccessUI(gate.state);
});
await gate.recheck(); // re-run the check
const session = await initRevTurbine({ /* ... */ });
const banner = session.placement({ surfaceSlot: { id: 'top_banner' } });
banner.onChange(() => {
const el = document.getElementById('banner');
if (!el) return;
el.style.display = banner.visible ? 'block' : 'none';
if (banner.content) {
el.innerHTML = banner.content.headline || '';
}
});
await banner.load();
document.getElementById('dismiss-btn')?.addEventListener('click', () => {
banner.dismiss();
});

| Concern | React hooks | Headless controllers | |---|---|---| | State management | React useState | Manual via onChange() | | Lifecycle | useEffect | Explicit load() / check() | | Cleanup | Hook unmount | Call unsub() / reset() | | Framework | React only | Any JS runtime | | Bundle | Includes React dep | Zero framework deps |

The React hooks (usePlacement, useEntitlement) delegate to the headless controllers internally — they share the same business logic.

Explore headless scenarios in the interactive playground: