This is the full developer documentation for RevTurbine SDK # Start > One command, one , a working feature gate in about ten minutes with no account — then connect Stripe and change monetization without a deploy. RevTurbine is monetization for product-led SaaS in one SDK: plans, entitlements, usage limits, trials, and the upgrade prompts that convert — decided per user, inside your app, from a Playbook you keep. We add no latency and our availability never affects your product: the SDK evaluates your Playbook locally, so decisions never depend on our servers. This page is the whole happy path. It runs in local mode first — no account, no key — and ends with hosted mode, where the same Playbook changes without a deploy. **Note — Not using React?:** The walkthrough is React because that is the shortest path to something on screen. The engine is framework-agnostic: `import { initRevTurbine } from '@revturbine/sdk/headless'` pulls in no React and runs on Svelte, Vue, plain TypeScript, and Node. Same checks, same decisions; you render the resolved content yourself. See [Headless API](/revturbine-sdk/guides/headless-api/) and [Server-Side Integration](/revturbine-sdk/getting-started/server-side/). Python and Rust SDKs evaluate the same Playbook with byte-identical results. ## Two ideas to hold Every decision RevTurbine makes combines two things, and the SDK combines them locally: - The **Playbook** is what you authored, the same for every user: [plans](/revturbine-sdk/concepts/runtime-and-playbook/#plan) and their [variations](/revturbine-sdk/concepts/runtime-and-playbook/#plan-variation), [entitlements](/revturbine-sdk/concepts/runtime-and-playbook/#entitlement) and the [rules](/revturbine-sdk/concepts/runtime-and-playbook/#entitlement-rule) that grant them, [segments](/revturbine-sdk/concepts/runtime-and-playbook/#segment), and [placements](/revturbine-sdk/concepts/runtime-and-playbook/#placement) with their [payloads](/revturbine-sdk/concepts/runtime-and-playbook/#placement-payload). It is one versioned file. - The **Runtime** is who is asking right now: the [UserContext](/revturbine-sdk/concepts/runtime-and-playbook/#usercontext), their [usage](/revturbine-sdk/concepts/runtime-and-playbook/#usage), and the [entitlement grants](/revturbine-sdk/concepts/runtime-and-playbook/#entitlement-grant) that result. [AccountContext](/revturbine-sdk/concepts/runtime-and-playbook/#accountcontext) is planned, not shipped. Plain-English definitions, how initialization and context changes drive a decision, and a worked example are on [Runtime and Playbook](/revturbine-sdk/concepts/runtime-and-playbook/). The code for every runtime is on [Initialize the SDK](/revturbine-sdk/getting-started/initialization/). ## Integration questions Use these focused guides when moving beyond the first gate: - [SDK upgrade behavior](/revturbine-sdk/guides/sdk-upgrades/): mirrors, denial reasons, gates, and presentation changes. - [Plans without subscriptions](/revturbine-sdk/guides/plans-without-subscriptions/): explicit free-plan policy and hosted context precedence. - [Visitor, user, and account identity](/revturbine-sdk/guides/identity-lifecycle/): sign-in, sign-out, and account switches. - [Usage periods and resets](/revturbine-sdk/guides/usage-periods/): application accounting and atomic enforcement. - [Billing facts versus app events](/revturbine-sdk/guides/events/#billing-facts-and-application-events): Stripe ownership and event identity. - [Mounted React gates](/revturbine-sdk/getting-started/react/#keeping-mounted-gates-current): reactive updates without remounting. - [Warnings and diagnostics](/revturbine-sdk/guides/troubleshooting/#warnings-that-need-an-integration-fix): what to fix when initialization or decisions degrade. - [Custom CTA actions and prices](/revturbine-sdk/guides/custom-cta-actions/): authored versus resolved shape and currency units. - [First CLI upload](/revturbine-sdk/reference/cli/#first-upload-of-a-starter-playbook): tenant selection and local evaluation limits. - [Parameter entitlements](/revturbine-sdk/guides/entitlements/#parameter-entitlements): application-read metadata rather than access gates. These pages are also included in the generated `llms-full.txt` for coding agents. ## 1. Bootstrap **With your coding agent** Paste this into Claude Code, Cursor, or whichever agent you use. The scaffold installs the RevTurbine agent skills, so the agent knows the rest. ```text Run `npm create revturbine@latest` in this project. Then follow the revturbine-start-here skill: wrap the app in in local mode with the generated revturbine.playbook.json, and replace ONE non-critical hard-coded plan check with . Do not touch server-side enforcement. Show me the diff before you finish. ``` Starting a new app instead? Scaffold it with its framework first (for example `create-next-app`), then paste this: ```text Run `npm create revturbine@latest` in this new app. Then follow the revturbine-start-here skill and build monetization in from the start: put every plan, limit and upgrade prompt in revturbine.playbook.json, never in app code, and run `npx revturbine validate ./revturbine.playbook.json` after each change. Gate features with or can(), and re-check anything paid on the server. Show me the Playbook and the diff before you finish. ``` The fuller prompts for both cases — and the CLI loop an agent uses to keep changing monetization afterwards — are on [Add RevTurbine with a Coding Agent](/revturbine-sdk/guides/coding-agent/). **By hand** ```bash npm create revturbine@latest ``` That installs `@revturbine/sdk`, pins `@revturbine/cli` to the project, drops a local-mode `revturbine.playbook.json` with a free and a pro plan, one gated feature and one upgrade prompt, and installs the agent skills. `npx revturbine create` and `npx @revturbine/cli init` run the same command. Already have an account? `revturbine download --live --save ./revturbine.playbook.json` pulls your real Playbook instead. ## 2. Wrap your app Local mode, no account: the Playbook you just generated is the whole configuration. ```tsx title="src/App.tsx" import { RevTurbineProvider } from '@revturbine/sdk'; import playbook from './revturbine.playbook.json'; import { useMemo } from 'react'; export default function App() { const options = useMemo(() => ({ localRuntime: { playbook }, user: { id: 'user_123', plan_handle: 'starter' }, }), []); return ( ); } ``` `plan_handle` is the plan's handle from your Playbook — the value plan-scoped rules match on. In hosted mode it arrives from Stripe on its own (step 7); here you supply it. ## 3. Gate a feature `` renders its children when the user is entitled. When they aren't, it renders the upgrade prompt your Playbook configures for that gate — copy, offer and CTA authored in config, not in your app. ```tsx title="src/components/BatchExport.tsx" import { Gate } from '@revturbine/sdk'; export function BatchExport() { return ( ); } ``` Start the dev server. With the starter Playbook's free plan, the gate shows the upgrade prompt; change `plan_handle` to `'pro'` and the button appears. ## 4. Check and protect in code For anything that isn't a React subtree — an event handler, a keyboard shortcut, a server route — ask the SDK directly. ```tsx import { useRevTurbine } from '@revturbine/sdk'; function ExportMenu() { const { sdk: rt } = useRevTurbine(); async function onExport() { // Ask: the answer, and nothing else. const access = await rt.can('batch_export'); // Ask and act: runs exportPdf only when entitled; hands back the // entitlement so you can open the upgrade prompt otherwise. const gated = await rt.gate('export_pdf', () => exportPdf()); if (!gated.ran) openUpgrade(gated.entitlement); } return ; } ``` `can()` returns `{ status, allowed, reason }` — plus `limit` / `used` / `remaining` when a usage or credit rule matched. See [Entitlements](/revturbine-sdk/guides/entitlements/). ## 5. Add a slot A `` is a spot where RevTurbine may show something — a banner, a modal, a toast, a button. Which placement fills it, for which user, with what copy and how often, is decided from the Playbook: targeting, caps, cooldowns and priority between competing prompts are the engine's job, not yours. ```tsx title="src/components/Dashboard.tsx" import { Slot } from '@revturbine/sdk'; export function Dashboard() { return (

Dashboard

{/* An upgrade banner for the users the Playbook targets — or nothing */}
); } ``` When a user clicks a CTA inside a placement, the SDK calls a resolver you provide — `navigate_to_plans`, `open_checkout_modal`, and so on — so navigation and checkout stay in your code. The starter Playbook uses `navigate_to_plans`; add one line to `options`: ```tsx docs-check=false reason="one option line; the full initRevTurbine options object is shown above" uiPathResolvers: { navigate_to_plans: () => { window.location.href = '/pricing'; } }, ``` Every action type and what it carries: [Placements → UI paths](/revturbine-sdk/guides/placements/). ## 6. Report usage and track events Usage limits and credits need to know the balance; the Playbook decides what to do about it. ```ts await rt.update({ usage: { ai_credits_used: 42 } }); // absolute balance, not a delta await rt.track('ai_generation_completed', { credits: 3 }); ``` Balances drive quota meters, low-credit warnings and limit prompts. Events feed targeting and analytics. ## 7. Verify on your server The browser check drives the UI; your server makes the money-safe decision before paid work runs — with the same SDK, evaluating the same Playbook in-process. ```ts import { initRevTurbine, RuntimeMode } from '@revturbine/sdk/headless'; import playbook from './revturbine.playbook.json'; export async function POST(request: Request): Promise { const user = await getSignedInUser(request); // your own auth const rt = await initRevTurbine({ runtimeMode: RuntimeMode.LocalOnly, localRuntime: { playbook }, // loaded once per process; `revturbine download --live` on deploy user: { id: user.id, plan_handle: user.planHandle }, }); const result = await rt.can('batch_export'); if (!result.allowed) return Response.json({ error: 'upgrade_required' }, { status: 402 }); return runExport(request); } ``` One instance per request, built from a Playbook you loaded once, so the check makes no network call. Why the re-check, and which actions need it: [Client vs Server Enforcement](/revturbine-sdk/concepts/enforcement/). Why a server does not use hosted mode, and what it can report: [Server-Side Integration](/revturbine-sdk/getting-started/server-side/). ## 8. Go hosted: connect Stripe, change it without a deploy Everything above works forever in local mode, free. Hosted mode is where you stop deploying to change monetization. 1. **Create an account and connect the CLI.** [revturbine.com/app](https://revturbine.com/app), then `revturbine login` (a browser device flow — no keys to paste). 2. **Connect Stripe** in **Settings → Stripe**. From then on a purchase updates what the user can do with no app code: plan and payment state arrive through a client session. Put `revturbine_account_id` and `revturbine_user_id` in the metadata of every Stripe customer you create so each payer links to an account from day one: [Link Stripe customers to accounts](/revturbine-sdk/guides/billing/#link-stripe-customers-to-accounts). See [Billing & Stripe](/revturbine-sdk/guides/billing/). 3. **Switch the provider to hosted mode.** Mint an ingest key under **Settings → API Token Management → Ingest Keys** and add your app's origins to its allowed origins. Then add the **client-session route** to your server (it mints a short-lived token with your server key, so the plan Stripe holds reaches the SDK) and pass both to the provider: ```tsx import { RevTurbineProvider } from '@revturbine/sdk'; import { useMemo, type ReactNode } from 'react'; import { fetchClientSession } from './revturbine-client-session'; export function RevTurbine({ userId, children }: { userId: string; children: ReactNode }) { const options = useMemo(() => ({ publicKey: process.env.NEXT_PUBLIC_REVTURBINE_PUBLIC_KEY!, // public key, safe in the browser user: { id: userId }, // plan_handle now comes from Stripe clientSession: fetchClientSession, // calls YOUR route; see Client sessions }), [userId]); return {children}; } ``` The route and the `fetchClientSession` function are on [Client sessions](/revturbine-sdk/guides/client-sessions/); every initialization variant is on [Initialize the SDK](/revturbine-sdk/getting-started/initialization/). 4. **Change something and reload.** Open the dashboard, edit the banner's copy or the free plan's export limit, launch the release. The SDK picks it up on its next refresh — no deploy. Or do it from the terminal: `revturbine download --live`, edit, `revturbine validate`, `revturbine diff --live`, `revturbine launch`. That loop — see what converted, change the Playbook, launch — is the product. Your agent can drive it too: [Add RevTurbine with a Coding Agent](/revturbine-sdk/guides/coding-agent/). ## Why RevTurbine **Decisions, not just rendering** Caps, cooldowns, eligibility, and priority across competing placements are decided for you. One `` returns the winner. No client-side orchestration, no homegrown segment logic. **Entitlements and prompts in one engine, tied to billing** A `` surfaces usage warnings, trial nudges and lifecycle prompts tied to billing state; `rt.can()` confirms or denies access. Gate a feature and show the upgrade prompt that converts it, from one Playbook. **Changed by product, not by a deploy** Drop a slot once. Copy, targeting, limits, offers and prices change in the dashboard or through the CLI; resolvers keep navigation in your code. Every launch is a versioned release you can roll back. **The same answer everywhere** `@revturbine/sdk/headless` imports no React; Python and Rust evaluate the same Playbook. A cross-language gate diffs their decisions byte-for-byte, so a check answers identically wherever you make it. ## Integration reference | Task | Guide | |---|---| | Initialize React and inspect readiness | [React Integration](/revturbine-sdk/getting-started/react/#userevturbine) | | Run checks without React | [TypeScript / Node](/revturbine-sdk/guides/headless-api/), [Python](/revturbine-sdk/getting-started/python/), [Rust](/revturbine-sdk/getting-started/rust/) | | Choose Playbook delivery and optional wiring | [Runtime Modes](/revturbine-sdk/guides/runtime-modes/), [Configuration](/revturbine-sdk/reference/configuration/) | | Match your app's colors | [Theming](/revturbine-sdk/guides/theming/) | | Route blocked actions to a paywall | [Paywall Host Pattern](/revturbine-sdk/guides/paywall-host/) | | Explain a missing placement or denied check | [Troubleshooting](/revturbine-sdk/guides/troubleshooting/), [Debugging Decisions](/revturbine-sdk/operate/debugging/) | | Check release changes before upgrading | [SDK changelog](https://github.com/revt-eng/revturbine-sdk/blob/main/CHANGELOG.md) | Coding agents can fetch individual pages from this index. [llms-small.txt](/revturbine-sdk/llms-small.txt) is an abridged documentation bundle, including the language guides; [llms-full.txt](/revturbine-sdk/llms-full.txt) contains the complete documentation. ## Next steps - [Recommended API Path](/revturbine-sdk/getting-started/api-overview/) — `can()` vs `gate()` vs ``, and the vocabulary - [Entitlements](/revturbine-sdk/guides/entitlements/) — feature gates, usage limits, credits, seats, tiers - [Placements](/revturbine-sdk/guides/placements/) — targeting, caps, cooldowns, priority, and UI paths - [Runtime Modes](/revturbine-sdk/guides/runtime-modes/) — local mode, hosted mode, and what is cached - [Interactive Playground](/revturbine-sdk/playground/) — every scenario, live and editable - [API reference](/revturbine-sdk/api/) — full TypeDoc reference for all exports ## Backend and edge examples Use the [cloud integration tutorial](/revturbine-sdk/tutorials/vercel-edge-entitlements/) for runnable Vercel, Firebase, AWS, Google Cloud and Azure adapters. It covers trusted user context, Playbook freshness, local entitlement checks, and server enforcement. # BrowserStorage Wraps a browser `Storage` object (`localStorage` or `sessionStorage`) with try/catch guards for quota and security-policy errors. ## Implements - `RevTurbineStorage` # CtaResolverRegistry Registry mapping CTA action types to resolver functions. Customers register a [CtaResolver](/revturbine-sdk/api/type-aliases/ctaresolver/) against an action type — typically a tenant-defined custom action name surfaced through `parseUiPath` — so the [PlacementRenderer](/revturbine-sdk/api/functions/placementrenderer/) can dispatch a CTA click to dedicated logic instead of the generic `onCtaClick` callback. Built-in action types may also be overridden by registering a resolver for them. ## Methods ### clear() > **clear**(): `void` Remove every registered resolver. #### Returns `void` *** ### get() > **get**(`type`): [`CtaResolver`](/revturbine-sdk/api/type-aliases/ctaresolver/) \| `undefined` Look up the resolver registered for an action type, if any. #### Parameters ##### type `string` #### Returns [`CtaResolver`](/revturbine-sdk/api/type-aliases/ctaresolver/) \| `undefined` *** ### has() > **has**(`type`): `boolean` Whether a resolver is registered for an action type. #### Parameters ##### type `string` #### Returns `boolean` *** ### register() > **register**(`type`, `resolver`): `void` Register a resolver for an action type. Re-registering the same type replaces the previous resolver (and warns), matching the slot-type registry's override semantics. #### Parameters ##### type `string` ##### resolver [`CtaResolver`](/revturbine-sdk/api/type-aliases/ctaresolver/) #### Returns `void` *** ### unregister() > **unregister**(`type`): `boolean` Remove the resolver for an action type. Returns true if a resolver was registered and removed. #### Parameters ##### type `string` #### Returns `boolean` # PlacementController Framework-agnostic placement lifecycle controller. Encapsulates the same register → decide → impression-track → interact flow that the React `usePlacement()` hook provides, but without any framework dependency. Subscribe to state changes with [onChange](/revturbine-sdk/api/classes/placementcontroller/#onchange) and read the current state with [state](/revturbine-sdk/api/classes/placementcontroller/#state), or just `await load()` for a one-shot pattern. ## Example ```ts const ctrl = new PlacementController(sdk, { surfaceSlot: { id: 'upsell_banner' } }); ctrl.onChange(() => updateUI(ctrl.state)); await ctrl.load(); ``` ## Accessors ### content #### Get Signature > **get** **content**(): `RevTurbineDecisionContent` \| `null` Current resolved content, or `null` if no decision has been loaded. ##### Returns `RevTurbineDecisionContent` \| `null` *** ### decision #### Get Signature > **get** **decision**(): `RevTurbinePlacementDecision` \| `null` Current decision, or `null` if not yet loaded. ##### Returns `RevTurbinePlacementDecision` \| `null` *** ### placementId #### Get Signature > **get** **placementId**(): `string` Registered placement ID, or empty string before first `load()`. ##### Returns `string` *** ### state #### Get Signature > **get** **state**(): [`PlacementControllerState`](/revturbine-sdk/api/interfaces/placementcontrollerstate/) Current state snapshot. ##### Returns [`PlacementControllerState`](/revturbine-sdk/api/interfaces/placementcontrollerstate/) *** ### visible #### Get Signature > **get** **visible**(): `boolean` Convenience: `true` when the current decision says the placement is visible. ##### Returns `boolean` ## Methods ### ctaClick() > **ctaClick**(`ctaTarget?`): `Promise`\<`void`\> Record a CTA click interaction. #### Parameters ##### ctaTarget? `string` #### Returns `Promise`\<`void`\> *** ### ctaComplete() > **ctaComplete**(`ctaTarget?`): `Promise`\<`void`\> Record a CTA completion interaction and hide the placement. #### Parameters ##### ctaTarget? `string` #### Returns `Promise`\<`void`\> *** ### dismiss() > **dismiss**(`cooldownMs?`): `Promise`\<`void`\> Record a dismissal. #### Parameters ##### cooldownMs? `number` Optional explicit window. Omit it — which is the normal case — and the SDK resolves the payload's authored `caps.cooldown_days`, falling back to the 7-day default. This used to default to 24h here and pass it ALWAYS, so the authored value and the 7-day default were both unreachable from React (plan 233 TASK-8b). #### Returns `Promise`\<`void`\> *** ### dispose() > **dispose**(): `void` Stop listening for Playbook loads and user-context changes. Idempotent; call it when the owning component unmounts (`usePlacement` does). #### Returns `void` *** ### load() > **load**(): `Promise`\<`RevTurbinePlacementDecision` \| `null`\> Register the placement (if needed) and fetch a decision. When `autoTrackImpression` is true (default), a visible decision automatically records an impression event. #### Returns `Promise`\<`RevTurbinePlacementDecision` \| `null`\> *** ### markRendered() > **markRendered**(): `void` Called when the placement's visual root renders (plan 144 TASK-11). Emits `placement_rendered` once, and under `placementExposure: 'render'` credits the presentation now. Idempotent. #### Returns `void` *** ### markVisible() > **markVisible**(`basis?`): `void` Called by the viewport-exposure substrate when the placement's visual root first enters the viewport — or immediately with `'render_fallback'` when `IntersectionObserver` is unavailable (plan 144 TASK-9/11 / AC-9, AC-10). Emits `placement_exposed` once with the basis, and under `placementExposure: 'viewport'` a viewport exposure credits the presentation here (Q-8). Idempotent. #### Parameters ##### basis? `ExposureBasis` = `'viewport'` how exposure was established; defaults to `'viewport'` #### Returns `void` *** ### onChange() > **onChange**(`listener`): () => `void` Subscribe to state changes. Returns an unsubscribe function. #### Parameters ##### listener [`ChangeListener`](/revturbine-sdk/api/type-aliases/changelistener/) #### Returns () => `void` #### Example ```ts const unsub = ctrl.onChange(() => console.log(ctrl.state)); // later... unsub(); ``` *** ### refresh() > **refresh**(): `Promise`\<`RevTurbinePlacementDecision` \| `null`\> Re-fetch the placement decision (clears impression + render + exposure tracking). #### Returns `Promise`\<`RevTurbinePlacementDecision` \| `null`\> *** ### remindMeLater() > **remindMeLater**(`seconds?`): `Promise`\<`void`\> Alias for [snooze](/revturbine-sdk/api/classes/placementcontroller/#snooze). #### Parameters ##### seconds? `number` #### Returns `Promise`\<`void`\> *** ### reset() > **reset**(): `void` Reset the controller state (clears decision, placement ID, etc.). #### Returns `void` *** ### snooze() > **snooze**(`seconds?`): `Promise`\<`void`\> Record a snooze / remind-me-later. #### Parameters ##### seconds? `number` Optional explicit window. Omit it and the SDK resolves the payload's authored `remind_later_minutes`, falling back to the tenant default (plan 233 TASK-8c). Remind-later is a DIFFERENT window from dismiss and resolves independently. #### Returns `Promise`\<`void`\> *** ### watchUserContext() > **watchUserContext**(): () => `void` Re-decide whenever the user context changes (BL-0004). The counterpart to EntitlementGate.watchUserContext, and the reason `sdk.convert()` now removes a converted placement in place: conversion writes no suppression — eligibility is a function of the user's CURRENT plan and targeting — so the only thing that can take a converted upgrade banner off screen is a re-decide against the reloaded context. Without this the placement stayed mounted until something else happened to re-resolve it, and the host had to call `refresh()` itself or use the component's `ctaComplete()` (the limitation plan 236 TASK-13 recorded). Only re-decides a controller that has already resolved something: before the first `load()` there is nothing on screen to be stale, and firing then would turn every startup `identify()` into a duplicate decision. Calls `load()` rather than `refresh()` on purpose — `refresh()` clears impression/exposure tracking, so a context change would re-credit a presentation the user never saw again. Returns an unsubscribe function; [dispose](/revturbine-sdk/api/classes/placementcontroller/#dispose) also drops it. #### Returns () => `void` # PlacementTypeRegistry Registry for placement slot types. Manages built-in and custom placement renderers. The registry resolves a PlacementOutput to the best matching slot type based on surface type and template, then provides the component and resolved props for rendering. ## Methods ### get() > **get**(`id`): [`PlacementSlotType`](/revturbine-sdk/api/interfaces/placementslottype/)\<[`PlacementSlotProps`](/revturbine-sdk/api/interfaces/placementslotprops/)\<[`ResolvedContent`](/revturbine-sdk/api/type-aliases/resolvedcontent/)\>\> \| `undefined` Look up a slot type by its id. #### Parameters ##### id `string` #### Returns [`PlacementSlotType`](/revturbine-sdk/api/interfaces/placementslottype/)\<[`PlacementSlotProps`](/revturbine-sdk/api/interfaces/placementslotprops/)\<[`ResolvedContent`](/revturbine-sdk/api/type-aliases/resolvedcontent/)\>\> \| `undefined` *** ### has() > **has**(`id`): `boolean` Check if a slot type id is registered. #### Parameters ##### id `string` #### Returns `boolean` *** ### listAll() > **listAll**(): [`PlacementSlotType`](/revturbine-sdk/api/interfaces/placementslottype/)\<[`PlacementSlotProps`](/revturbine-sdk/api/interfaces/placementslotprops/)\<[`ResolvedContent`](/revturbine-sdk/api/type-aliases/resolvedcontent/)\>\>[] List all registered slot types. #### Returns [`PlacementSlotType`](/revturbine-sdk/api/interfaces/placementslottype/)\<[`PlacementSlotProps`](/revturbine-sdk/api/interfaces/placementslotprops/)\<[`ResolvedContent`](/revturbine-sdk/api/type-aliases/resolvedcontent/)\>\>[] *** ### listByComponentType() > **listByComponentType**(`componentType`): [`PlacementSlotType`](/revturbine-sdk/api/interfaces/placementslottype/)\<[`PlacementSlotProps`](/revturbine-sdk/api/interfaces/placementslotprops/)\<[`ResolvedContent`](/revturbine-sdk/api/type-aliases/resolvedcontent/)\>\>[] List slot types for a specific surface type. #### Parameters ##### componentType `"inline"` \| `"custom"` \| `"push"` \| `"banner"` \| `"modal"` \| `"tooltip"` \| `"sidebar"` \| `"toast"` \| `"fullscreen"` \| `"email"` \| `"sms"` \| `"in_page"` \| `"button"` \| `"full_page"` \| `"agent"` \| `"cli"` #### Returns [`PlacementSlotType`](/revturbine-sdk/api/interfaces/placementslottype/)\<[`PlacementSlotProps`](/revturbine-sdk/api/interfaces/placementslotprops/)\<[`ResolvedContent`](/revturbine-sdk/api/type-aliases/resolvedcontent/)\>\>[] *** ### ~~listBySurfaceType()~~ > **listBySurfaceType**(`componentType`): [`PlacementSlotType`](/revturbine-sdk/api/interfaces/placementslottype/)\<[`PlacementSlotProps`](/revturbine-sdk/api/interfaces/placementslotprops/)\<[`ResolvedContent`](/revturbine-sdk/api/type-aliases/resolvedcontent/)\>\>[] :::caution[Deprecated] Use [listByComponentType](/revturbine-sdk/api/classes/placementtyperegistry/#listbycomponenttype). ::: #### Parameters ##### componentType `"inline"` \| `"custom"` \| `"push"` \| `"banner"` \| `"modal"` \| `"tooltip"` \| `"sidebar"` \| `"toast"` \| `"fullscreen"` \| `"email"` \| `"sms"` \| `"in_page"` \| `"button"` \| `"full_page"` \| `"agent"` \| `"cli"` #### Returns [`PlacementSlotType`](/revturbine-sdk/api/interfaces/placementslottype/)\<[`PlacementSlotProps`](/revturbine-sdk/api/interfaces/placementslotprops/)\<[`ResolvedContent`](/revturbine-sdk/api/type-aliases/resolvedcontent/)\>\>[] *** ### register() > **register**\<`P`\>(`options`): `void` Register a placement slot type. If a type with the same id already exists, it is replaced (allows customer overrides of built-in types). #### Type Parameters ##### P `P` *extends* [`PlacementSlotProps`](/revturbine-sdk/api/interfaces/placementslotprops/)\<[`ResolvedContent`](/revturbine-sdk/api/type-aliases/resolvedcontent/)\> #### Parameters ##### options [`RegisterPlacementSlotTypeOptions`](/revturbine-sdk/api/type-aliases/registerplacementslottypeoptions/)\<`P`\> #### Returns `void` *** ### resolve() > **resolve**(`output`): [`PlacementSlotType`](/revturbine-sdk/api/interfaces/placementslottype/)\<[`PlacementSlotProps`](/revturbine-sdk/api/interfaces/placementslotprops/)\<[`ResolvedContent`](/revturbine-sdk/api/type-aliases/resolvedcontent/)\>\> \| `undefined` Resolve the best matching slot type for a placement output. Resolution order: 1. If the output's surface.template matches a registered type id, use it 2. Find all types for the output's surface.type, sort by priority descending, and pick the first whose `accepts()` returns true 3. Fall back to a 'custom' type if registered 4. Return undefined if no match #### Parameters ##### output #### Returns [`PlacementSlotType`](/revturbine-sdk/api/interfaces/placementslottype/)\<[`PlacementSlotProps`](/revturbine-sdk/api/interfaces/placementslotprops/)\<[`ResolvedContent`](/revturbine-sdk/api/type-aliases/resolvedcontent/)\>\> \| `undefined` *** ### unregister() > **unregister**(`id`): `boolean` Unregister a placement slot type by id. Returns true if the type was found and removed. #### Parameters ##### id `string` #### Returns `boolean` # RevTurbineApiError Error thrown when an API request fails. ## Extends - `Error` # RevTurbineBuiltinDimensionsError Error thrown when a built-in dimension read ([RevTurbineServer.getBuiltinDimensions](/revturbine-sdk/api/classes/revturbineserver/#getbuiltindimensions)) is refused by the control plane. Like [RevTurbineSeatAssignmentError](/revturbine-sdk/api/classes/revturbineseatassignmenterror/) it carries only the HTTP status, a typed [BuiltinDimensionsErrorReason](/revturbine-sdk/api/type-aliases/builtindimensionserrorreason/), the control plane's machine-readable error `code` when one was returned, and a correlation id — never the server key, request headers, or the free-text response message. ## Extends - `Error` ## Properties ### code? > `readonly` `optional` **code?**: `string` The control plane's machine-readable error code, when the body carried one. *** ### reason > `readonly` **reason**: [`BuiltinDimensionsErrorReason`](/revturbine-sdk/api/type-aliases/builtindimensionserrorreason/) Typed classification of [RevTurbineBuiltinDimensionsError.status](/revturbine-sdk/api/classes/revturbinebuiltindimensionserror/#status). *** ### requestId > `readonly` **requestId**: `string` Correlation id sent as `x-request-id` on the failed request. *** ### status > `readonly` **status**: `number` HTTP status returned by the control plane. # RevTurbineClientSessionError Error thrown when a client-session mint request is rejected by the control plane. Carries only the HTTP status and a correlation id — deliberately never the server key, request headers, or response body — so the key cannot leak through error logs (plan 157 AC-8). ## Extends - `Error` ## Properties ### requestId > `readonly` **requestId**: `string` Correlation id for the failed request. *** ### status > `readonly` **status**: `number` HTTP status returned by the control plane. # RevTurbineCustomerSdk The core RevTurbine customer-facing SDK. Provides methods for: - **Identity** — `identify()`, `resetIdentity()` - **Placements** — `registerPlacement()`, `getPlacementDecision()`, `getPlacement()` - **Entitlements** — `checkEntitlement()`, `updateUsage()` - **Trials** — `getTrialStatus()` - **Events** — `track()` / `capture()` (generic lane; platform-name collisions are namespaced), `emitPlatformEvent()` (typed platform lane, contract-validated payloads) - **Interactions** — `trackTreatmentInteraction()`, `dismiss()`, `convert()` - **Context** — `setUserContext()`, `setPageContext()`, `refreshPageContext()` ## Example ```ts import { initRevTurbine } from '@revturbine/sdk'; const session = await initRevTurbine({ publicKey: 'rtk_…', mode: 'react', }); session.identify('user_123', { plan_handle: 'pro' }); const branding = session.sdk.getBranding(); ``` ## Methods ### can() > **can**(`handle`, `context?`): `Promise`\<\{ \}\> Check whether the user can do something — the advertised alias of [checkEntitlement](/revturbine-sdk/api/classes/revturbinecustomersdk/#checkentitlement). Returns the rich EntitlementResult (`allowed`, `status`, `reason`, and `limit`/`used`/`remaining` for limit-bearing entitlements). This client check is a UX convenience — for billing-critical or abuse-sensitive actions, re-run the same check on your server before granting access. #### Parameters ##### handle `string` ##### context? `RevTurbineEntitlementContext` #### Returns `Promise`\<\{ \}\> #### Example ```ts const access = await rt.can('generate_image'); if (!access.allowed) showUpgrade(); @public ``` *** ### checkEntitlement() > **checkEntitlement**(`handle`, `context?`): `Promise`\<\{ \}\> Evaluate a single entitlement handle for the active user, purely from the loaded UserContext and Playbook. The advertised [can](/revturbine-sdk/api/classes/revturbinecustomersdk/#can) alias wraps this with the friendly verb name. A check that lands before the Playbook does waits out the in-flight load, bounded, and re-evaluates (BL-0179); it fails CLOSED only once nothing more is coming. See [RevTurbinePlaybookLoadState](/revturbine-sdk/api/type-aliases/revturbineplaybookloadstate/). #### Parameters ##### handle `string` ##### context? `RevTurbineEntitlementContext` #### Returns `Promise`\<\{ \}\> *** ### checkoutMetadata() > **checkoutMetadata**(`outputId`): [`RevTurbineCheckoutMetadata`](/revturbine-sdk/api/interfaces/revturbinecheckoutmetadata/) \| `null` The Stripe Checkout `revturbine_*` metadata bag for a rendered output (plan 282 REQ-5; research §6.2), keyed by the `output_id` of a decision this SDK produced — what the customer's server passes when it creates the Checkout Session the output's CTA leads to. Pass `metadata` as the session's `metadata` AND as `subscription_data.metadata`. The session copy is what `checkout.session.completed` carries; the subscription copy is what every later `customer.subscription.*` event carries and what each invoice snapshots under `parent.subscription_details.metadata`, so renewals, failures and recoveries stay attributable without a `customers` row. Pass `client_reference_id` as the session's `client_reference_id`, Stripe's own caller-side key, which the identity bridge also reads as the user id. Checkout cannot set Customer metadata inline: if you let Checkout create the Customer, follow up with `customers.update({ metadata })` — see the billing guide. Every value is one this SDK holds; nothing is minted. The user id is the one `identify()` set, as this SDK's own telemetry carries it (an email-shaped id is hashed the same way on every lane, so the purchase joins the click). The account id is present only when an account was identified — never the user-derived fallback. A key the SDK does not hold is absent, not empty. #### Parameters ##### outputId `string` #### Returns [`RevTurbineCheckoutMetadata`](/revturbine-sdk/api/interfaces/revturbinecheckoutmetadata/) \| `null` `null` when no decision from this SDK produced `outputId`. *** ### convert() > **convert**(`outputId`): `Promise`\<`void`\> Record a confirmed CTA completion, then reload the UserContext so every mounted surface re-decides against the plan the user just converted to. Conversion is **not** a suppression: it writes no cooldown and no permanent retirement (spec §"interaction" table; Kent's plan 254 D-9 ruling). What makes a converted upgrade banner leave the screen is the user no longer matching it — so a conversion that did not refresh the context left the placement mounted until something else happened to re-resolve it. Kent, 2026-09-25 (BL-0004): *"Converting should trigger a reloading of UserContext with the new plan/billing state the user has converted to."* Ordering, and why it is this way round: 1. The interaction is recorded (analytics + attribution), as before. 2. **Optimistically**, locally: when the converted output's CTA names a target plan (`cta_path.plan_handle`), the held context moves to that plan immediately. Billing truth arrives by Stripe webhook, so `GET /api/sdk/client-context` can still report the OLD plan for seconds after a successful checkout — waiting for it would leave the upgrade prompt on screen exactly when the user has just paid. 3. Subscribers are notified either way — a conversion whose CTA names no plan still re-decides mounted slots and re-checks mounted gates, which is what turns a plan change the host applied itself into a re-render. 4. **Then** the server refresh (`fetchClientContext()`), which overlays the RevTurbine-authoritative trial / billing-health / plan fields and notifies again if it changed anything. It is best-effort and never throws, so awaiting it cannot fail a conversion; with no client-session minter configured it is a no-op and the optimistic state stands. Mounted `PlacementController`s re-decide and mounted `EntitlementGate`s re-check through their `watchUserContext()` subscriptions — which the React bindings attach for you. Re-resolution emits the normal lifecycle events once per final decision; the transient states emit nothing. `PlacementController.ctaComplete()` is unchanged and remains the component-level path. #### Parameters ##### outputId `string` #### Returns `Promise`\<`void`\> *** ### diagnoseSlotInventory() > **diagnoseSlotInventory**(): [`RevTurbineSlotInventoryDiagnosis`](/revturbine-sdk/api/interfaces/revturbineslotinventorydiagnosis/) Diff the authored Playbook against what this app actually mounted, in both directions. Plan 233 TASK-10 / AC-12. Both directions matter and they fail differently: - **authoredButUnmounted** — a placement targets a slot no code renders. It can never show. Nothing reports this today; the decision path is never even asked about a slot that was never mounted, so there is no error, no warning, and no reason code. This is the silent one. - **mountedButUnauthored** — a slot is mounted that no placement targets. It renders its fallback forever, which usually means a typo in the slot id or config that was never deployed. This is a diagnostic and deliberately reports rather than throws: it is run against a live app, and an integration with a stale slot should keep working while someone reads the output. A config-side audit **cannot** produce this. That is the lesson from the August diagnosis of the escalated integration, which concluded the fixed slots were unmounted: they were mounted, and the call sites passed slot ids that no placement targeted. Only the running app knows what it mounted. #### Returns [`RevTurbineSlotInventoryDiagnosis`](/revturbine-sdk/api/interfaces/revturbineslotinventorydiagnosis/) *** ### dismiss() > **dismiss**(`outputId`): `Promise`\<`void`\> Suppress a decision's placement for its authored dismiss cooldown. Terminal for the window: `getPlacementDecision` will not return it again until the cooldown lapses. #### Parameters ##### outputId `string` #### Returns `Promise`\<`void`\> *** ### dispose() > **dispose**(): `void` Stop background clickstream flushing and release page-unload listeners. Flushes any buffered events one last time. Call when tearing down an SDK instance (e.g. SPA unmount) to avoid a dangling interval/listeners. #### Returns `void` *** ### emitPlatformEvent() > **emitPlatformEvent**\<`K`\>(`eventName`, `payload`, `options?`): `Promise`\<`void`\> Emit a typed platform event (plan 228 TASK-4). The sanctioned first-party path for every platform event OUTSIDE the control plane — SDK lifecycle, gates, placements, milestones, growth signals, and the R-2 billing vocabulary (a billing fact consumed from Stripe is equally emittable here; the ingest path dedupes on the contract's identity keys, e.g. `billing_ref`). The payload parameter type is inferred from the event's payload contract, so the API cannot drift from the schema the payload is validated against. The name is sent RAW — no `clickstream_*` namespacing — which is exactly what distinguishes this from [track](/revturbine-sdk/api/classes/revturbinecustomersdk/#track): the generic lane namespaces platform collisions, the typed lane IS the platform. In development/test builds the payload is validated against its contract at emit time; a violation emits an `sdk_validation_warning` alongside (never a throw — telemetry must not break the host app). Production builds skip validation; the ingest boundary re-validates authoritatively. #### Type Parameters ##### K `K` *extends* [`EmittablePlatformEventName`](/revturbine-sdk/api/type-aliases/emittableplatformeventname/) #### Parameters ##### eventName `K` ##### payload `EventPayloadInput`\<`K`\> ##### options? `RevTurbineEventOptions` #### Returns `Promise`\<`void`\> *** ### emitSemantic() > **emitSemantic**(`eventType`, `payload`, `options?`): `Promise`\<`void`\> Emit a semantic event through the GENERIC string lane. Since plan 228 TASK-4 this is a customer-lane API, not a platform one: it rides capture, so a name colliding with platform vocabulary is namespaced `clickstream_*` — platform events cannot be forged from here. The SDK's own platform emissions use [emitPlatformEvent](/revturbine-sdk/api/classes/revturbinecustomersdk/#emitplatformevent), whose payloads are typed and contract-validated. The fields are passed to capture DIRECTLY, exactly as a customer's own `capture(name, {...})` call would. Anything else double-wraps them. This used to send `{semantic: true, payload: fields}`. The wire mapping already stores an envelope's properties under the key `payload`, so that inner wrapper landed the fields at `properties.payload.payload.` in `events_clickstream` — one level deeper than every `capture()` event and than the canonical carrier this SDK documents. Readers that followed the documented shape (`user_context_fields`, any analytics pipe) found nothing on a semantic event, which is why gate telemetry was invisible to the entitlement metrics despite being emitted correctly. The `semantic: true` marker went with it: it was written here and read nowhere in the SDK, the web app, or any pipe. It marked nothing and cost a whole nesting level. `liftClickstreamFields` / `pickClickstreamField` are unaffected — they resolve top-level first and fall back to the nested bag, so they read the fields either way. Only the STORED JSON changes shape, and it changes to the shape everything else already expects. #### Parameters ##### eventType `string` ##### payload [`SdkEventProperties`](/revturbine-sdk/api/type-aliases/sdkeventproperties/) ##### options? `RevTurbineEventOptions` #### Returns `Promise`\<`void`\> *** ### emitTrigger() > **emitTrigger**(`trigger`, `payload?`, `options?`): `Promise`\<`void`\> Emit a canonical trigger event. There is no in-SDK consumer that matches on trigger names — the shipped behavior is ingest to `events_clickstream` only (plan 250's wire/consumer matrix); a decision-engine placement trigger matches `trigger.slot_id` against registered slots, not this event's name. Convenience wrapper over [emitSemantic](/revturbine-sdk/api/classes/revturbinecustomersdk/#emitsemantic) that constrains the event name to [RevTurbineTriggerEvent](/revturbine-sdk/api/type-aliases/revturbinetriggerevent/) and attaches standard context (user_id, plan, timestamp) automatically. **Three trigger names land namespaced on the wire.** This rides the generic emit lane, which prefixes `clickstream_` to any name the platform taxonomy already declares — the rule that stops a client forging a first-party fact by naming an event after one. `trial_expired`, `payment_failed` and `feature_gated` collide, so they are stored as `clickstream_trial_expired`, `clickstream_payment_failed` and `clickstream_feature_gated`. Query those names, not the raw ones. The remaining triggers are unaffected, and the platform's own versions of the three are emitted first-party through the typed surface. Pinned by `trigger-namespacing-parity.test.ts` (plan 231 REQ-4). #### Parameters ##### trigger `"payment_failed"` \| `"onboarding_complete"` \| `"trial_midpoint"` \| `"trial_expiring"` \| `"trial_expired"` \| `"usage_limit_approaching"` \| `"usage_limit_reached"` \| `"credit_balance_low"` \| `"seat_limit_reached"` \| `"feature_gated"` \| `"cancel_intent"` \| `"auto_renewal_reminder"` \| `"invite_teammate_prompt"` \| `"referral_offer"` \| `"plan_upgrade_nudge"` ##### payload? [`RevTurbineTriggerPayload`](/revturbine-sdk/api/type-aliases/revturbinetriggerpayload/) ##### options? `RevTurbineEventOptions` #### Returns `Promise`\<`void`\> #### Example ```ts await sdk.emitTrigger('usage_limit_approaching', { usage_percent: 85, threshold: 80 }); await sdk.emitTrigger('trial_expiring', { days_remaining: 2 }); // stored as `clickstream_feature_gated` — see the collision note above await sdk.emitTrigger('feature_gated', { feature: 'advanced_automation' }); ``` *** ### explainPlacementDecision() > **explainPlacementDecision**(`input`): `Promise`\<[`RevTurbinePlacementDecisionExplanation`](/revturbine-sdk/api/interfaces/revturbineplacementdecisionexplanation/)\> Explain why a placement decision was selected. Returns a structured diagnostic payload that includes: - Segment membership evaluation with predicate-level pass/fail details - Entitlement-rule matching outcomes for current targeting context - Placement decision metadata (`rule_id`, reason codes, suppression) Useful for debuggers, QA tooling, and customer-facing explainability UIs. #### Parameters ##### input `RevTurbinePlacementDecisionInput` #### Returns `Promise`\<[`RevTurbinePlacementDecisionExplanation`](/revturbine-sdk/api/interfaces/revturbineplacementdecisionexplanation/)\> *** ### fetchClientContext() > **fetchClientContext**(`clientToken?`): `Promise`\<`void`\> Enrich the held UserContext with the user's server-known CLIENT-SAFE context (plan 157). Fetches `GET /api/sdk/client-context` with the provided client token (`rt_client_`, minted by the customer's backend via the server SDK's `clientSessions.create`), maps the client-safe fields (trial state, coarse billing health) into the UserContext, and merges them in — these fields are RevTurbine-authoritative, so they refresh the held context. The token is held in memory only (never storage / URL / logs). Enrichment is best-effort: a network error or non-200 never throws into the app; on 401 (expired / revoked) the held token is cleared. Server-side entitlement checks remain authoritative regardless of what the browser sees. #### Parameters ##### clientToken? `string` the `rt_client_` token; when omitted, reuses the last one. #### Returns `Promise`\<`void`\> *** ### fetchUserContext() > **fetchUserContext**(`userId`): `Promise`\<`UserTargetingContext`\> Fetch the resolved user context from the decision API. Returns the user's matched segments, traits, plan, and usage — used to determine which placement payloads are eligible for display. #### Parameters ##### userId `string` #### Returns `Promise`\<`UserTargetingContext`\> *** ### flushEvents() > **flushEvents**(): `Promise`\<`void`\> Flush any buffered clickstream events to `POST /api/track` immediately. Called on the size threshold, the [RevTurbineEventBatchingOptions](/revturbine-sdk/api/interfaces/revturbineeventbatchingoptions/) interval timer, and page-unload. Best-effort and non-throwing — delivery failures are swallowed (plan 95 REQ-3). Safe to call when the buffer is empty (no-op). #### Returns `Promise`\<`void`\> *** ### gate() > **gate**\<`T`\>(`action`, `fn`, `context?`): `Promise`\<[`RevTurbineGateResult`](/revturbine-sdk/api/type-aliases/revturbinegateresult/)\<`T`\>\> Gate an action behind an entitlement — the advertised `gate(action, fn)` verb. Checks the entitlement for `action`; if allowed, runs `fn` and returns its result; otherwise does NOT run `fn` and returns the entitlement so the caller can surface a paywall (e.g. render an ``). See [RevTurbineGateResult](/revturbine-sdk/api/type-aliases/revturbinegateresult/). #### Type Parameters ##### T `T` #### Parameters ##### action `string` ##### fn () => `T` \| `Promise`\<`T`\> ##### context? `RevTurbineEntitlementContext` #### Returns `Promise`\<[`RevTurbineGateResult`](/revturbine-sdk/api/type-aliases/revturbinegateresult/)\<`T`\>\> #### Example ```ts const gated = await rt.gate('export_pdf', () => exportPdf()); if (!gated.ran) openPaywall(gated.entitlement); @public ``` *** ### getBranding() > **getBranding**(): [`ResolvedBranding`](/revturbine-sdk/api/interfaces/resolvedbranding/) Resolve the branding to render with, down the four-rung ladder (plan 118): the explicit `branding` init option → the config's deprecated embedded `theme` (dev-warns) → the `apiBranding` init option → [DEFAULT\_BRANDING](/revturbine-sdk/api/variables/default_branding/). Branding is a display concern only — it never affects placement decisions or entitlement checks. The result is always complete, so callers can render unconditionally even when no branding was supplied. #### Returns [`ResolvedBranding`](/revturbine-sdk/api/interfaces/resolvedbranding/) The merged branding and the ladder rung it came from. *** ### getEffectiveUserContext() > **getEffectiveUserContext**(): `Promise`\<`Readonly`\<[`RevTurbineUserContext`](/revturbine-sdk/api/interfaces/revturbineusercontext/)\>\> Return the immutable ephemeral context currently entering local evaluation. #### Returns `Promise`\<`Readonly`\<[`RevTurbineUserContext`](/revturbine-sdk/api/interfaces/revturbineusercontext/)\>\> *** ### getEligibleAddons() > **getEligibleAddons**(): `Promise`\<[`EligibleAddon`](/revturbine-sdk/api/interfaces/eligibleaddon/)[]\> Return the public add-on variations eligible for the active user. #### Returns `Promise`\<[`EligibleAddon`](/revturbine-sdk/api/interfaces/eligibleaddon/)[]\> *** ### getEligiblePlans() > **getEligiblePlans**(): `Promise`\<[`EligiblePlan`](/revturbine-sdk/api/interfaces/eligibleplan/)[]\> Return the public plan variations eligible for the active user. #### Returns `Promise`\<[`EligiblePlan`](/revturbine-sdk/api/interfaces/eligibleplan/)[]\> *** ### getEntitlements() > **getEntitlements**(): `Record`\<`string`, `EntitlementResult`\> Return the SDK-resolved entitlement snapshot for the active user. The returned object is keyed by entitlement handle and reflects the latest results from local/runtime checks tracked by the SDK. #### Returns `Record`\<`string`, `EntitlementResult`\> *** ### getExperimentVariant() > **getExperimentVariant**(`experimentHandle`): `Promise`\<`ExperimentVariantSelection`\> Resolve the normalized variant used by RevTurbine decisions and telemetry. Missing, conflicting, and unavailable assignments remain explicit and are never converted to a control variant. #### Parameters ##### experimentHandle `string` #### Returns `Promise`\<`ExperimentVariantSelection`\> *** ### ~~getExportedConfig()~~ > **getExportedConfig**(): \{ \} \| `undefined` :::caution[Deprecated] Renamed to [RevTurbineCustomerSdk.getPlaybook](/revturbine-sdk/api/classes/revturbinecustomersdk/#getplaybook) — `Playbook` is the canonical name for the artifact (BL-0156). Identical behaviour; removed in `0.13.0`. ::: #### Returns \{ \} \| `undefined` *** ### getPersonalizationTokens() > **getPersonalizationTokens**(`payload?`): `RevTurbinePersonalizationTokens` Return the SDK-resolved personalization token map. Reads from the transient `personalization` map on the user context, which holds both SDK-derived tokens (plan_name, usage_current, etc.) and app-provided tokens set via `setPersonalization()` or `identify()`. Pass the placement's payload (its `recommendation_strategy` / `recommendation_plan_override` fields) to resolve the `{{recommended_plan_handle}}` / `{{recommended_plan_name}}` tokens for that specific placement (plan #47, Appendix C.3). Without a payload the map carries the user-level `next_tier_up` default; with one, the placement's authored strategy (e.g. a `custom` forced plan) overlays those two tokens. All other tokens are unaffected. #### Parameters ##### payload? Optional placement payload carrying the per-placement recommendation strategy. Omit for the user-level default. ###### recommendation_plan_override? `string` \| `null` ###### recommendation_strategy? `RecommendationStrategy` \| `null` #### Returns `RevTurbinePersonalizationTokens` *** ### getPlacement() > **getPlacement**(`config`): `Promise`\<\{ \} \| `null`\> Resolve the legacy placement payload for a request config. Prefer [getPlacementDecision](/revturbine-sdk/api/classes/revturbinecustomersdk/#getplacementdecision) for new integrations; kept for the documented legacy request-config path. #### Parameters ##### config `RevTurbinePlacementRequestConfig` #### Returns `Promise`\<\{ \} \| `null`\> *** ### getPlacementDecision() > **getPlacementDecision**(`input`): `Promise`\<`RevTurbinePlacementDecision`\> Resolve the placement decision for a registered surface slot — the evaluated payload (or lack of one), its reason, and the metadata needed to report the interaction lifecycle ([trackTreatmentInteraction](/revturbine-sdk/api/classes/revturbinecustomersdk/#tracktreatmentinteraction), [dismiss](/revturbine-sdk/api/classes/revturbinecustomersdk/#dismiss), [snooze](/revturbine-sdk/api/classes/revturbinecustomersdk/#snooze), [convert](/revturbine-sdk/api/classes/revturbinecustomersdk/#convert)). #### Parameters ##### input `RevTurbinePlacementDecisionInput` #### Returns `Promise`\<`RevTurbinePlacementDecision`\> *** ### getPlaybook() > **getPlaybook**(): \{ \} \| `undefined` Returns the Playbook snapshot loaded at initialization. Canonical consumers should retain their own Playbook or use [normalizeConfigArtifactOrThrow](/revturbine-sdk/api/functions/normalizeconfigartifactorthrow/) at their ingestion boundary rather than reading it back out of the SDK. #### Returns \{ \} \| `undefined` *** ### getPlaybookLoadFailure() > **getPlaybookLoadFailure**(): [`RevTurbinePlaybookLoadFailure`](/revturbine-sdk/api/interfaces/revturbineplaybookloadfailure/) \| `undefined` Why the hosted Playbook has not loaded, or `undefined` when nothing failed (BL-0012). A failed load is otherwise indistinguishable from "not loaded yet": checks answer `config_unavailable` either way. This names the failing step, the URL, the HTTP status and — for a 401/403 or a missing `publicKey` — the credential to check. A failed refresh while a previously loaded Playbook is still in use is reported here too; the SDK keeps deciding with the last-known-good Playbook. #### Returns [`RevTurbinePlaybookLoadFailure`](/revturbine-sdk/api/interfaces/revturbineplaybookloadfailure/) \| `undefined` #### Example ```ts if (!(await rt.can('export')).allowed) { const failure = rt.getPlaybookLoadFailure(); if (failure) console.error('RevTurbine Playbook did not load:', failure.message); } ``` *** ### getPolicy() > **getPolicy**(): [`RevTurbinePolicySnapshot`](/revturbine-sdk/api/interfaces/revturbinepolicysnapshot/) Return current SDK policy snapshot. #### Returns [`RevTurbinePolicySnapshot`](/revturbine-sdk/api/interfaces/revturbinepolicysnapshot/) *** ### getRegisteredSlots() > **getRegisteredSlots**(): [`RevTurbineRegisteredSlot`](/revturbine-sdk/api/interfaces/revturbineregisteredslot/)[] Every surface slot this SDK instance currently has mounted. Plan 233 TASK-10. `verify-integration` names the gap this closes in its own text: *"A placement targeting a slot no code renders can never show, and nothing reports this; there is no probe, so walk the Playbook's placement triggers against your inventory of rendered slots."* There was no way to obtain that inventory at runtime — the registry is private and keyed by a hashed id, not by the slot id the author wrote. `slotId` is the author-facing identity (what a Playbook's `trigger.slot_id` must match), not the internal hash. That distinction is the whole point: a diff against authored config is meaningless in hash space. #### Returns [`RevTurbineRegisteredSlot`](/revturbine-sdk/api/interfaces/revturbineregisteredslot/)[] *** ### getTargeting() > **getTargeting**(): [`RevTurbineTargeting`](/revturbine-sdk/api/interfaces/revturbinetargeting/) Return the SDK-resolved targeting snapshot for the active user. Includes user id, segment ids, traits, plan, and merged usage so demo surfaces can display the same context used by placement eligibility. #### Returns [`RevTurbineTargeting`](/revturbine-sdk/api/interfaces/revturbinetargeting/) *** ### getTelemetryConsent() > **getTelemetryConsent**(): [`TelemetryConsent`](/revturbine-sdk/api/type-aliases/telemetryconsent/) The current behavior-telemetry consent state (plan 144 TASK-8). #### Returns [`TelemetryConsent`](/revturbine-sdk/api/type-aliases/telemetryconsent/) *** ### getTelemetryCounters() > **getTelemetryCounters**(): `TelemetryCounters` A snapshot of the telemetry pipeline counters (plan 144 TASK-8) — created / dropped / redacted / sampled / deduped / queued / sent / failed. A cheap diagnostic for "why did my events not arrive?". Returns a copy, so mutating it never touches the SDK's live tallies. #### Returns `TelemetryCounters` *** ### getTrialStatus() > **getTrialStatus**(): `Promise`\<\{ \}\> Return the active user's trial status, derived from their trial instances and the configured reverse-trial rules. #### Returns `Promise`\<\{ \}\> *** ### getUsage() > **getUsage**(): [`RevTurbineUsageSnapshot`](/revturbine-sdk/api/type-aliases/revturbineusagesnapshot/) Return the SDK-resolved usage snapshot for the active user. Keys are usage units (derived from configured usage token prefixes when available), and values include current usage plus optional limit when known. #### Returns [`RevTurbineUsageSnapshot`](/revturbine-sdk/api/type-aliases/revturbineusagesnapshot/) *** ### getUserContext() > **getUserContext**(): `object` Build the full persistence-ready UserContext from the current SDK state. Includes `tenant_id` and `user_id` required for API storage. Also projects the decision-driving fields — `plan_handle`, `builtin_dimensions`, `trial` (the resolved trial view), `tiers`, `payment_failed`, `payment_at_risk` and `instances` — when held. The snapshot is a copy: changing it changes nothing in the SDK; write through `update()` / `setUserContext()`. #### Returns `object` *** ### hydrate() > **hydrate**(`payload`): `void` Hydrate the SDK with a server-evaluated payload. Call this on the client after receiving a `ServerEvaluationPayload` from the server-side SDK. Applies entitlements, trial status, usage, and user context. Placement decisions are evaluated locally against the current UserContext and Playbook: legacy payloads do not bind their decisions to the producer's complete evaluation context. #### Parameters ##### payload #### Returns `void` #### Example ```ts // In your React component / page hydration: const session = await initRevTurbine({ publicKey, mode: 'react' }); session.sdk.hydrate(serverPayload); ``` *** ### identify() > **identify**\<`T`\>(`userId`, `context?`): `void` Identify the current user and (optionally) supply their context. Accepts the canonical [IdentifyContextInput](/revturbine-sdk/api/type-aliases/identifycontextinput/) — recognized by the presence of `account_id` / `email` / `plan` / `plan_handle` / `usage` / `entitlements` / `custom` — or a legacy plain-traits object, which is stored under `custom` unchanged. `plan_handle` is THE plan matching identity (the plan's `unique_handle`, plan 191 REQ-1); the `plan` object is display metadata and never participates in matching, and nothing in `custom` ever drives plan resolution (REQ-2). Guardrails: an empty id rejects the call; an email-shaped id warns in dev (ids stay opaque — pass emails as `{ email }`); unrecognized top-level keys are named in a dev warning instead of being dropped or routed silently. Legacy-traits behavior is unchanged beyond the diagnostic. `account_id` is OPTIONAL. Identify no account and every event this SDK emits carries `user-fallback:` instead — the acting user's id behind the [FALLBACK\_ACCOUNT\_ID\_PREFIX](/revturbine-sdk/api/variables/fallback_account_id_prefix/) namespace marker, never a bare user id (BL-0117, Kent's D-13). `events_clickstream.account_id` and `placement_presentations.account_id` are the join keys `monetization_funnel` matches accounts on, so the prefix is what lets the warehouse keep a fabricated key out of an account denominator instead of counting it as a real account. Identify an account when you have one. #### Type Parameters ##### T `T` *extends* [`IdentifyContextInput`](/revturbine-sdk/api/type-aliases/identifycontextinput/) #### Parameters ##### userId `string` ##### context? `Exact`\<[`IdentifyContextInput`](/revturbine-sdk/api/type-aliases/identifycontextinput/), `T`\> #### Returns `void` #### Example ```ts rt.identify('user_123', { plan_handle: 'pro' }); // THE matching identity rt.identify('user_123', { plan_handle: 'pro', plan: { handle: 'pro', name: 'Professional' } }); ``` *** ### onUserContextChange() > **onUserContextChange**(`listener`): () => `void` Subscribe to user-context changes — `identify()`, `setUserContext()`, `update()`, `updateUsage()`, and `resetIdentity()` (plan 194 REQ-3). Returns an unsubscribe function. Listeners must never throw; one that does is caught here rather than allowed to break the verb that fired it. #### Parameters ##### listener () => `void` #### Returns () => `void` #### Example ```ts const unsubscribe = rt.onUserContextChange(() => refreshMyUi()); ``` *** ### persistPlacementTypes() > **persistPlacementTypes**(`types`): `Promise`\<`void`\> Persist authored placement types to a configured backend. **Requires an explicit `placementTypes` endpoint override.** RevTurbine hosts no `/api/sdk/placement-types` route — the default path was never built, and there is no table behind it — so without an override this method is a no-op rather than a guaranteed 404. It previously threw `persist_placement_types_failed:404` in every non-`local_only` mode, which made a documented method unusable on the documented default path. Supply `endpointOverrides.placementTypes` (or override the provider hook of the same name) to send these somewhere real. #### Parameters ##### types [`RevTurbinePlacementTypeEntity`](/revturbine-sdk/api/interfaces/revturbineplacementtypeentity/)[] #### Returns `Promise`\<`void`\> *** ### recordAccountCreated() > **recordAccountCreated**(`payload`): `Promise`\<\{ `reason`: `string` \| `null`; `recorded`: `boolean`; \}\> Record that a customer account came into existence (plan 276 REQ-3). Requires the account grain, the creation time, the creating act and the evidence that proves it. A **user** signup is not account creation (plan 276 R-2) — emit `user_signed_up` for that; a payload whose `source` names a user-grain act is rejected without emitting, and the result says so. #### Parameters ##### payload `AccountCreatedPayload` #### Returns `Promise`\<\{ `reason`: `string` \| `null`; `recorded`: `boolean`; \}\> *** ### recordClickThru() > **recordClickThru**(`placementId`, `payloadId?`, `surfaceTemplateId?`, `metadata?`, `cooldownMs?`): `Promise`\<`void`\> Record a bare placement click-through — the user clicked the CTA but did not confirm the conversion (e.g. abandoned checkout). Time-boxed like a dismiss; the placement may return (plan 167). For a confirmed conversion use [recordConversion](/revturbine-sdk/api/classes/revturbinecustomersdk/#recordconversion). #### Parameters ##### placementId `string` The placement's stable rule_id. ##### payloadId? `string` Optional payload variant id. ##### surfaceTemplateId? `string` Optional surface template id. ##### metadata? [`RevTurbineImpressionMetadata`](/revturbine-sdk/api/type-aliases/revturbineimpressionmetadata/) Optional metadata persisted with the record. ##### cooldownMs? `number` Optional explicit cooldown window (ms). Defaults to 7 days. #### Returns `Promise`\<`void`\> *** ### recordConversion() > **recordConversion**(`placementId`, `payloadId?`, `surfaceTemplateId?`, `metadata?`): `Promise`\<`void`\> Record a confirmed conversion — the user completed the CTA action. The record is retained for analytics. Future eligibility is evaluated from the current UserContext and Playbook. #### Parameters ##### placementId `string` The placement's stable rule_id. ##### payloadId? `string` Optional payload variant id. ##### surfaceTemplateId? `string` Optional surface template id. ##### metadata? [`RevTurbineImpressionMetadata`](/revturbine-sdk/api/type-aliases/revturbineimpressionmetadata/) Optional metadata persisted with the record. #### Returns `Promise`\<`void`\> *** ### recordDismissal() > **recordDismissal**(`placementId`, `payloadId?`, `surfaceTemplateId?`, `metadata?`, `cooldownMs?`): `Promise`\<`void`\> Record a placement dismissal — the user explicitly closed it ("No thanks"). Time-boxed: the placement is hidden for the dismiss cooldown (`cooldown_after_dismiss_days`, default 7 days) and re-shows once it elapses (plan 167). NOT permanent — for a confirmed conversion use [recordConversion](/revturbine-sdk/api/classes/revturbinecustomersdk/#recordconversion). #### Parameters ##### placementId `string` The placement's stable rule_id. ##### payloadId? `string` Optional payload variant id. ##### surfaceTemplateId? `string` Optional surface template id. ##### metadata? [`RevTurbineImpressionMetadata`](/revturbine-sdk/api/type-aliases/revturbineimpressionmetadata/) Optional metadata persisted with the record. ##### cooldownMs? `number` Optional explicit cooldown window (ms). Defaults to 7 days. #### Returns `Promise`\<`void`\> *** ### recordImpression() > **recordImpression**(`placementId`, `payloadId?`, `surfaceTemplateId?`, `metadata?`): `Promise`\<`void`\> Record that a placement was rendered to the user. Plan 43 TASK-9. Writes an `impressed` record to the SDK's `ImpressionHistory`, which persists to `localStorage` via `StorageImpressionStore` (or in-memory storage in non-browser environments). The impression contributes to: - **Frequency caps (`cap.v1`)** — once-per-period limits count this impression against the user's quota for the placement. - **Trial milestone supersession analytics** — for trial_progress ladders, the supersession diagnostic uses delivery state to distinguish "replaced an undelivered placement" (counts in `superseded_placement_ids`) from "lower threshold was already shown" (NOT counted — spec §3.5 "supersession only applies to undelivered placements"). - **Generic milestone supersession (`content.milestone_order`)** — the order-based variant in `applyContentMilestoneSupersession`. Call this from consumer code when the placement is actually shown to the user (e.g., from a React component's `useEffect` on mount, or after the rendering call returns). The SDK does NOT auto-record impressions — surfaces are rendered by consumer code, so only the consumer knows when a placement was actually presented (vs. fetched but not displayed). Safe to call multiple times for the same placement — duplicates append additional impression records (used by frequency caps to count delivery events). #### Parameters ##### placementId `string` The placement's stable rule_id (e.g. `'pl_trial_progress_70'`). Match `decision.output.rule_id` from `getPlacementDecision`. ##### payloadId? `string` Optional payload variant id, for variant-level analytics. ##### surfaceTemplateId? `string` Optional surface template id, for per-surface cap accounting. ##### metadata? [`RevTurbineImpressionMetadata`](/revturbine-sdk/api/type-aliases/revturbineimpressionmetadata/) Optional metadata persisted with the record. #### Returns `Promise`\<`void`\> #### Example ```ts const decision = await sdk.getPlacementDecision({ placementId: 'slot_trial_modal' }); if (decision.visible && decision.output?.rule_id) { renderBanner(decision.output); await sdk.recordImpression(decision.output.rule_id, decision.output.output_id); } ``` *** ### recordTrialRevision() > **recordTrialRevision**(`episode`, `labels?`): `Promise`\<`TrialRevisionRecordResult`\> Record one app-owned trial-episode revision (plan 276 R-1(b), TASK-13). **Trial execution and ownership stay with your app.** RevTurbine runs no trial, enrols nobody and decides nothing about when a trial ended; it records the fact your app states. Pass the episode's facts — including the `evidence` that proves the revision — and the SDK runs the same pure classifier every RevTurbine port runs, then emits one `trial_revision` on the typed platform lane if, and only if, the evidence supports one. The classifier reads **no clock**. An episode whose scheduled end has merely passed emits nothing and comes back `pending_unknown`: an elapsed deadline proves only that the time passed, never that your app ended anything. Supply `actual_end_at` and an `end_evidence` when it did. In `local_only` mode the SDK makes no server calls, so the fact reaches any registered event consumer and never RevTurbine; the result says `recorded_locally` rather than pretending otherwise. Never throws, and never blocks a decision. #### Parameters ##### episode `TrialEpisodeFacts` The episode's facts, as your app holds them. ##### labels? `TrialRevisionLabels` = `{}` Playbook vocabulary that rides along for grouping: `rule_handle`, `plan_handle`, `trial_type`, `provider_ref`. None of it affects the classification (plan 276 R-1(c)). #### Returns `Promise`\<`TrialRevisionRecordResult`\> *** ### ~~registerPlacement()~~ > **registerPlacement**(`config`): `Promise`\<`string`\> :::caution[Deprecated] Use `registerSurfaceSlot`. ::: #### Parameters ##### config [`RevTurbinePlacementConfig`](/revturbine-sdk/api/interfaces/revturbineplacementconfig/) #### Returns `Promise`\<`string`\> *** ### registerSurfaceSlot() > **registerSurfaceSlot**(`config`): `Promise`\<`string`\> Register a surface slot with the local runtime and return its placement id. **Client-local by default** (BL-0197): this performs no network write. The slot is recorded in the in-process registry, and the pipeline discovers it from the `slot_*` / `placement_*` telemetry the SDK emits through `/api/track`. A write happens only when `custom_endpoints.surfaceSlots` points at a slot-inventory service you run; there is no fallback to `/api/placements`. #### Parameters ##### config [`RevTurbineSurfaceSlotConfig`](/revturbine-sdk/api/interfaces/revturbinesurfaceslotconfig/) #### Returns `Promise`\<`string`\> *** ### reportSdkError() > **reportSdkError**(`reason`, `message?`): `void` Report that the SDK itself failed (plan 182 TASK-5). `sdk_error` was declared in the meta taxonomy with no emitter. This is the "the SDK malfunctioned" signal — distinct from `resolution_failure`, which means a *decision* produced nothing. It rides the same anonymous `events_sdk_meta` lane: no tenant, no user, no placement identity. Deduped on `reason` for the session and capped, mirroring emitResolutionFailure — an SDK failing inside a render loop must not become a telemetry flood. Best-effort and never throwing: this is called from `catch` blocks, so it must not manufacture a second failure. #### Parameters ##### reason `string` Stable, categorical cause (e.g. `provider_init_failed`) — what dashboards group by. ##### message? `string` Human-readable detail for triage, sent on the same footing as `sdk_validation_warning`'s message, which already ships on this lane. #### Returns `void` *** ### reset() > **reset**(): `void` Clear the current user — the advertised alias of [resetIdentity](/revturbine-sdk/api/classes/revturbinecustomersdk/#resetidentity) (e.g. on sign-out). #### Returns `void` *** ### resetIdentity() > **resetIdentity**(): `void` Reset to anonymous user state, re-inferring a fresh anonymous id. Also available as the advertised [reset](/revturbine-sdk/api/classes/revturbinecustomersdk/#reset) alias. #### Returns `void` *** ### resetUserContext() > **resetUserContext**(): `void` Hard-reset the user context to a blank slate — removes EVERY user-context value (`id`, `plan`, `email`, `account_id`, `custom`, `usage`, `entitlements`, `personalization`) plus usage balances, and clears the decision cache, interaction state, and impression history. Unlike [resetIdentity](/revturbine-sdk/api/classes/revturbinecustomersdk/#resetidentity) (a sign-out that re-infers anonymous context when the `inferUser` policy is on), this performs **no** inference, so the resulting context is guaranteed empty. Mostly for demo / fixture flows that reset cleanly between scenarios. #### Returns `void` #### Example ```ts // Between demo personas: rt.resetUserContext(); rt.identify('demo_pro', { plan_handle: 'pro' }); ``` *** ### setApiBranding() > **setApiBranding**(`branding`): `void` Supply the branding-API rung after init. `apiBranding` can be passed at construction when the host fetches branding itself, but with `fetchThemeOverride` the SDK fetches `GET /api/sdk/theme` *after* the instance exists. Without this setter that fetched value reached only the React theme context, so `getBranding()` and `useRevTurbineTheme()` could report different branding for the same tenant (plan 184). #### Parameters ##### branding \{ \} \| `undefined` Branding from the Branding API. Overrides config-embedded branding but never the explicit `branding` init option. #### Returns `void` *** ### setColorScheme() > **setColorScheme**(`scheme`): `void` Set the resolved display palette without rebuilding the SDK or its caches. Explicit branding retains precedence; this only selects embedded defaults. #### Parameters ##### scheme `"light"` \| `"dark"` Resolved light or dark preference. #### Returns `void` *** ### setTelemetryConsent() > **setTelemetryConsent**(`consent`): `void` Update behavior-telemetry consent at runtime (plan 144 TASK-8 / REQ-12). Takes effect on the next event with **no** provider remount: `denied` / `pending` stop event creation immediately, so nothing reaches ingest, registered consumers, or integrations; `granted` resumes emission. Does not affect the keyless anonymous SDK-init beacon, which has its own RevTurbineInitOptions.anonymousTelemetry switch (REQ-13). #### Parameters ##### consent [`TelemetryConsent`](/revturbine-sdk/api/type-aliases/telemetryconsent/) the new consent state #### Returns `void` *** ### setTrialInstances() > **setTrialInstances**(`instances`, `options?`): `Promise`\<\{ \}\> Derive and push the user's trial status from their TrialInstance records, resolving the matching free/reverse trial rule straight from the initialized RevTurbineConfig's `free_trial_rules` / `reverse_trial_rules`. The config-driven counterpart to setTrialStatus: instead of the host app pre-deriving a `UserTrialStatus`, the SDK evaluates the tenant's trial rules against the supplied instances via `@revt-eng/core`'s `evaluateTrialStatus` and pushes the result into the local trial context that gates `trial_progress` / `trial_ending` / `trial_ended` / `trial_converted` placements. The same evaluator runs in the Python server SDK, so both decide identically. Requires an initialized Playbook (static mode, or any mode with a config provider). Returns the derived status, or `{ in_trial: false }` when no active trial instance applies. #### Parameters ##### instances readonly `object`[] The user's trial instance records. ##### options? Optional `nowIso` clock pin (defaults to the current time) and `basePlanHandle` (the base plan a reverse-trial user reverts to, surfaced as `plan_handle`). ###### basePlanHandle? `string` ###### nowIso? `string` #### Returns `Promise`\<\{ \}\> *** ### setUserContext() > **setUserContext**\<`T`\>(`userContext`): `void` Merge fields into the session-bound user context. Recognized fields upsert; an omitted field never clobbers a previously-set value. See the advertised [update](/revturbine-sdk/api/classes/revturbinecustomersdk/#update) alias for the full recognized-field contract. Server-assigned fields (`seat_type_handle`, `activity_score`, `activity_score_computed_at`) are dropped and reported, never merged: they are written only by the tenant's backend or RevTurbine's enrichment (D-36, BL-0352). Since 0.12.0 the parameter is Exact, like [identify](/revturbine-sdk/api/classes/revturbinecustomersdk/#identify) and [update](/revturbine-sdk/api/classes/revturbinecustomersdk/#update): a key `RevTurbineUserContext` does not declare is a compile error even when the object arrives through a variable. Free-form values belong under `custom`. #### Type Parameters ##### T `T` *extends* [`RevTurbineUserContext`](/revturbine-sdk/api/interfaces/revturbineusercontext/) #### Parameters ##### userContext `Exact`\<[`RevTurbineUserContext`](/revturbine-sdk/api/interfaces/revturbineusercontext/), `T`\> #### Returns `void` *** ### snooze() > **snooze**(`outputId`, `seconds?`): `Promise`\<`void`\> Suppress a decision's placement for a "remind me later" window — `seconds` overrides the authored default. #### Parameters ##### outputId `string` ##### seconds? `number` #### Returns `Promise`\<`void`\> *** ### syncTrialRevisions() > **syncTrialRevisions**(`labels?`): `Promise`\<`TrialRevisionRecordResult`[]\> Record every revision the registered trial provider currently states. Resolves the `'trial'` domain provider, classifies each episode it returns and records the ones whose evidence supports a revision. An episode that resolves to `pending_unknown` is reported and emits nothing. With **no trial provider registered** this records nothing, logs the one-time notice naming the gap, and returns a single `no_trial_provider` result — never an error (ruling D-21). Call [RevTurbineCustomerSdk.recordTrialRevision](/revturbine-sdk/api/classes/revturbinecustomersdk/#recordtrialrevision) directly if you would rather push episodes than have them pulled. #### Parameters ##### labels? `TrialRevisionLabels` = `{}` #### Returns `Promise`\<`TrialRevisionRecordResult`[]\> *** ### track() > **track**(`name`, `data?`): `Promise`\<`void`\> Track an event — the advertised alias of trackEvent. Powers analytics, frequency caps, attribution, and experiments. Identity comes from the active user context ([identify](/revturbine-sdk/api/classes/revturbinecustomersdk/#identify) / [setUserContext](/revturbine-sdk/api/classes/revturbinecustomersdk/#setusercontext)). `account_id` carries the identified account when there is one and the labelled fallback `user-fallback:` when there is not — never a bare, unlabelled user id (BL-0117, Kent's D-13). #### Parameters ##### name `string` ##### data? [`SdkEventProperties`](/revturbine-sdk/api/type-aliases/sdkeventproperties/) #### Returns `Promise`\<`void`\> #### Example ```ts rt.track('ai_generation_completed', { credits: 3 }); @public ``` *** ### trackControlPlaneEvent() > **trackControlPlaneEvent**(`eventType`, `payload?`, `options?`): `Promise`\<`void`\> Emit a typed control-plane semantic event (plan 112). The dogfood-faithful surface for RevTurbine's own product activity: `eventType` is constrained to the canonical ControlPlaneEventType taxonomy and the `system`/`workflow` source classification is stamped automatically. Forwards through the same ingest + consumer path as capture — so the event lands in clickstream AND any registered analytics resolver (e.g. a PostHog provider from [createPostHogAnalyticsProvider](/revturbine-sdk/api/functions/createposthoganalyticsprovider/)). Identity comes from the active user context set via [identify](/revturbine-sdk/api/classes/revturbinecustomersdk/#identify) / [setUserContext](/revturbine-sdk/api/classes/revturbinecustomersdk/#setusercontext): the operator is `user_id` and the acting RevTurbine customer tenant is `account_id`. `tenant_id` is stamped server-side and is never carried on the event (plan 112 REQ-3/REQ-4). When no account has been identified, `account_id` carries the labelled fallback `user-fallback:` — a bare, unlabelled user id is never stamped into an account column (BL-0117, Kent's D-13). Classify it with [isFallbackAccountId](/revturbine-sdk/api/functions/isfallbackaccountid/). #### Parameters ##### eventType `"web_signed_up"` \| `"web_signed_in"` \| `"cli_signed_up"` \| `"cli_signed_in"` \| `"cli_command_executed"` \| `"playbook_version_submitted"` \| `"playbook_version_approved"` \| `"playbook_version_rejected"` \| `"playbook_version_deployed"` \| `"playbook_version_launched"` \| `"playbook_version_parked"` \| `"playbook_version_resumed"` \| `"playbook_version_discarded"` \| `"playbook_version_archived"` \| `"playbook_imported"` \| `"playbook_exported"` \| `"entity_created"` \| `"entity_updated"` \| `"entity_deleted"` \| `"web_api_error"` A canonical control-plane event type. ##### payload? [`SdkEventProperties`](/revturbine-sdk/api/type-aliases/sdkeventproperties/) = `{}` Optional event-specific properties (e.g. `{ resource, resource_id }`). ##### options? `RevTurbineEventOptions` Emit options, e.g. `{ immediate: true }` to bypass batching. #### Returns `Promise`\<`void`\> #### Example ```ts sdk.identify('operator_42', { account_id: 'tn_acme' }); await sdk.trackControlPlaneEvent('playbook_version_deployed', { change_set_id: 'cs_9' }); ``` *** ### trackTreatmentInteraction() > **trackTreatmentInteraction**(`input`): `Promise`\<`void`\> Report a treatment interaction directly by placement/treatment id — the lower-level counterpart to [dismiss](/revturbine-sdk/api/classes/revturbinecustomersdk/#dismiss)/[snooze](/revturbine-sdk/api/classes/revturbinecustomersdk/#snooze)/[convert](/revturbine-sdk/api/classes/revturbinecustomersdk/#convert), which key off a decision's `output_id` instead. `decisionId`, `segmentHandles` and `segmentIds` (plan 282 TASK-9) ride the interaction onto the wire as `decision_id` / `segment_handles` / `segment_ids`. All optional: given, they win; otherwise the SDK fills them from the output it indexed for `payloadId` (`segment_ids` only when a hosted context supplied minted ids — never in local mode); a caller that supplies none and names no indexed output sends exactly what it sent before. A `cta_clicked` on an indexed output whose CTA opens a plan picker or pricing page ([CHECKOUT\_STARTED\_CTA\_ACTION\_TYPES](/revturbine-sdk/api/variables/checkout_started_cta_action_types/)) also emits `checkout_started` — never for a CTA that opens checkout directly. #### Parameters ##### input `RevTurbineTreatmentInteractionInput` #### Returns `Promise`\<`void`\> *** ### update() > **update**\<`T`\>(`patch`): `void` Patch the session-bound user context — the advertised `update(patch)` verb. Accepts every session-scoped [RevTurbineUserContext](/revturbine-sdk/api/interfaces/revturbineusercontext/) field except the identity handle `id` (see [RevTurbineUpdateInput](/revturbine-sdk/api/type-aliases/revturbineupdateinput/)); to (re)establish identity use [identify](/revturbine-sdk/api/classes/revturbinecustomersdk/#identify). `usage` routes through [updateUsage](/revturbine-sdk/api/classes/revturbinecustomersdk/#updateusage) (absolute balances + usage threshold-crossing events); every other recognized field routes through [setUserContext](/revturbine-sdk/api/classes/revturbinecustomersdk/#setusercontext), which merges with upsert semantics — an omitted field never clobbers a previously-set value. Both stores are updated independently, so `update({ plan, usage })` applies each without interference. Keys outside the recognized set are dropped with a dev-only warning — they never reach the context, and `update({ : n })` is not a usage report. #### Type Parameters ##### T `T` *extends* [`RevTurbineUpdateInput`](/revturbine-sdk/api/type-aliases/revturbineupdateinput/) #### Parameters ##### patch `Exact`\<[`RevTurbineUpdateInput`](/revturbine-sdk/api/type-aliases/revturbineupdateinput/), `T`\> #### Returns `void` #### Examples ```ts // Bump reported usage: rt.update({ usage: { generations: 25 } }); ``` ```ts // Reflect a plan change and new traits in one call (identity unchanged): rt.update({ plan_handle: 'pro', custom: { role: 'admin' } }); @public ``` *** ### updateUsage() > **updateUsage**(`balances`): `void` Set absolute usage balances for the active user and emit any threshold-crossing triggers. See the advertised [update](/revturbine-sdk/api/classes/revturbinecustomersdk/#update) alias's `usage` key for the same contract via the general patch verb. #### Parameters ##### balances [`UsageBalances`](/revturbine-sdk/api/type-aliases/usagebalances/) #### Returns `void` *** ### validateUiPathResolvers() > **validateUiPathResolvers**(`options?`): `Promise`\<`RevTurbineUiPathResolverValidationReport`\> Validate that each configured UI path action has a resolver implementation. By default this validates `localRuntime.playbook.content_ui_paths` (when present) against: - `uiPathResolvers` passed at SDK init - optional `resolvers` passed to this method - CTA handlers from domain providers (`domain: 'cta'`), unless disabled #### Parameters ##### options? `RevTurbineUiPathResolverValidationOptions` = `{}` #### Returns `Promise`\<`RevTurbineUiPathResolverValidationReport`\> # RevTurbineDimensionWriteError Error thrown when a built-in dimension write ([RevTurbineServer.setBuiltinDimensions](/revturbine-sdk/api/classes/revturbineserver/#setbuiltindimensions)) is refused by the control plane (BL-0382). Like [RevTurbineSeatAssignmentError](/revturbine-sdk/api/classes/revturbineseatassignmenterror/) it carries only the HTTP status, a typed [DimensionWriteErrorReason](/revturbine-sdk/api/type-aliases/dimensionwriteerrorreason/), the control plane's machine-readable error `code` when one was returned, and a correlation id — never the server key, request headers, or the free-text response message. ## Extends - `Error` ## Properties ### code? > `readonly` `optional` **code?**: `string` The control plane's machine-readable error code, when the body carried one. *** ### reason > `readonly` **reason**: [`DimensionWriteErrorReason`](/revturbine-sdk/api/type-aliases/dimensionwriteerrorreason/) Typed classification of [RevTurbineDimensionWriteError.status](/revturbine-sdk/api/classes/revturbinedimensionwriteerror/#status). *** ### requestId > `readonly` **requestId**: `string` Correlation id sent as `x-request-id` on the failed request. *** ### status > `readonly` **status**: `number` HTTP status returned by the control plane. # RevTurbineSeatAssignmentError Error thrown when a seat-type assignment ([RevTurbineServer.assignSeatType](/revturbine-sdk/api/classes/revturbineserver/#assignseattype)) is refused by the control plane. Like [RevTurbineClientSessionError](/revturbine-sdk/api/classes/revturbineclientsessionerror/) it carries only the HTTP status, a typed [SeatAssignmentErrorReason](/revturbine-sdk/api/type-aliases/seatassignmenterrorreason/), the control plane's machine-readable error `code` when one was returned, and a correlation id — never the server key, request headers, or the free-text response message. ## Extends - `Error` ## Properties ### code? > `readonly` `optional` **code?**: `string` The control plane's machine-readable error code, when the body carried one. *** ### reason > `readonly` **reason**: [`SeatAssignmentErrorReason`](/revturbine-sdk/api/type-aliases/seatassignmenterrorreason/) Typed classification of [RevTurbineSeatAssignmentError.status](/revturbine-sdk/api/classes/revturbineseatassignmenterror/#status). *** ### requestId > `readonly` **requestId**: `string` Correlation id sent as `x-request-id` on the failed request. *** ### status > `readonly` **status**: `number` HTTP status returned by the control plane. # RevTurbineServer The RevTurbine server-side client. Holds the server key; mints client sessions ([RevTurbineServer.createClientSession](/revturbine-sdk/api/classes/revturbineserver/#createclientsession)), writes server-only user state ([RevTurbineServer.assignSeatType](/revturbine-sdk/api/classes/revturbineserver/#assignseattype)) and reads the user's server-resolved built-in dimensions ([RevTurbineServer.getBuiltinDimensions](/revturbine-sdk/api/classes/revturbineserver/#getbuiltindimensions)). ## Accessors ### clientSessions #### Get Signature > **get** **clientSessions**(): `object` Client-session minting namespace (plan 157). Ergonomic form of [createClientSession](/revturbine-sdk/api/classes/revturbineserver/#createclientsession): ##### Example ```ts const { client_token, expires_at } = await server.clientSessions.create({ subject: session.user.id, }); // return client_token to the frontend ``` ##### Returns `object` ###### create > **create**: (`input`) => `Promise`\<[`ClientSessionResult`](/revturbine-sdk/api/interfaces/clientsessionresult/)\> ###### Parameters ###### input [`CreateClientSessionInput`](/revturbine-sdk/api/interfaces/createclientsessioninput/) ###### Returns `Promise`\<[`ClientSessionResult`](/revturbine-sdk/api/interfaces/clientsessionresult/)\> ## Methods ### assignSeatType() > **assignSeatType**(`userId`, `seatTypeHandle`, `options?`): `Promise`\<\{ \}\> Assign a user's seat type, or clear it with `null` (plan 279 TASK-16a). Calls `POST /api/sdk/user-contexts` (`upsertServerUserContext`) with exactly `{ user_id, seat_type_handle }`, authenticated by the server key this client holds. The tenant is taken from that key — it is never a parameter, and no tenant header is sent. **Server-written only (D-36).** A user's seat-type assignment is written only by the tenant's backend through this server-key call (or by RevTurbine enrichment). It is never set from the browser: the client SDK's `identify()` / `update()` do not accept `seat_type_handle`, and the endpoint refuses every credential except a server key. Pass `options.builtinDimensions` (and `options.override`) to set built-in dimension values in the same write — see [setBuiltinDimensions](/revturbine-sdk/api/classes/revturbineserver/#setbuiltindimensions) for their semantics (BL-0382). #### Parameters ##### userId `string` The tenant's own identifier for the user (non-empty). ##### seatTypeHandle `string` \| `null` One of the tenant's current seat-type handles, or `null` to clear the user's assignment. ##### options? [`AssignSeatTypeOptions`](/revturbine-sdk/api/interfaces/assignseattypeoptions/) = `{}` Optional built-in dimension values to write with the seat. #### Returns `Promise`\<\{ \}\> The resulting state, including the stored `seat_type_handle`, the customer-set `builtin_dimensions`, the pinned `overrides` and `updated_at`. #### Throws if the control plane refuses the write — `reason` is `unknown_seat_type` for a 422 on the handle, `unknown_dimension_value` for a 422 on a dimension value, and `forbidden` for a 403 (the key is not a server key). The error never carries the key. #### Example ```ts await server.assignSeatType('user_123', 'admin'); await server.assignSeatType('user_123', null); // clear the assignment ``` *** ### createClientSession() > **createClientSession**(`input`): `Promise`\<[`ClientSessionResult`](/revturbine-sdk/api/interfaces/clientsessionresult/)\> Mint a short-lived, opaque per-user client-session token (plan 157; the server key is the minting authority, plan 256). The customer backend — which holds the server key (passed as [RevTurbineServerOptions.apiKey](/revturbine-sdk/api/interfaces/revturbineserveroptions/#apikey)) — calls this to obtain a browser-safe `rt_client_` token scoped to one end-user subject, then returns the token to its frontend. The frontend authenticates `GET /api/sdk/client-context` with it to read the user's client-safe context. This is a **server-only** capability: the browser SDK never mints tokens (it only consumes them). The tenant is derived server-side from the server key, never from this call. #### Parameters ##### input [`CreateClientSessionInput`](/revturbine-sdk/api/interfaces/createclientsessioninput/) #### Returns `Promise`\<[`ClientSessionResult`](/revturbine-sdk/api/interfaces/clientsessionresult/)\> #### Throws if the control plane rejects the mint. The error carries only the HTTP status + request id — never the key. *** ### getBuiltinDimensions() > **getBuiltinDimensions**(`userId`): `Promise`\<\{ \}\> Read the control plane's server-resolved built-in segment dimensions for one user (BL-0366). Calls `GET /api/sdk/user-contexts/{userId}/builtin-dimensions` (`getServerUserBuiltinDimensions`), authenticated by the server key this client holds. The tenant is taken from that key — it is never a parameter, and no tenant header is sent. The result's `builtin_dimensions` carries the values the browser SDK would receive through the client context (Subscription State, Trial Type, Activity State, Seat Type, Buyer Role, Billing Health, Email Type); `region` / `device_type` are request-derived and never present. An unknown user resolves to each dimension's default. Feed it to local evaluation as the server overlay: pass `result.builtin_dimensions` as `createStaticProviders({ …, userContext, serverBuiltinDimensions })`, which overlays it per key on `userContext.builtin_dimensions` — the server value wins (plan 279 PD-3). The Python and Rust server SDKs accept the same object through `server_builtin_dimensions`. #### Parameters ##### userId `string` The tenant's own identifier for the user (non-empty) — the same key `assignSeatType` and `createClientSession` use. #### Returns `Promise`\<\{ \}\> `{ tenant_id, user_id, builtin_dimensions, resolved_at }`. #### Throws if the control plane refuses the read — `reason` is `forbidden` for a 403 (the key is not a server key). The error never carries the key. #### Example ```ts const { builtin_dimensions } = await server.getBuiltinDimensions('user_123'); const providers = createStaticProviders({ config: playbook, userContext: { id: 'user_123' }, serverBuiltinDimensions: builtin_dimensions, }); ``` *** ### setBuiltinDimensions() > **setBuiltinDimensions**(`userId`, `dimensions`, `options?`): `Promise`\<\{ \}\> Set a user's built-in segment dimension values from your backend (BL-0382, rulings D-46 / D-47). Calls `POST /api/sdk/user-contexts` (`upsertServerUserContext`) with `{ user_id, builtin_dimensions, override }`, authenticated by the server key this client holds. The tenant is taken from that key — never a parameter. The write is an UPSERT per dimension: dimensions you do not name are left alone, and `null` clears a dimension (its value and its pin). The seat assignment is untouched (use [assignSeatType](/revturbine-sdk/api/classes/revturbineserver/#assignseattype)). Settable dimensions: `subscription_state`, `trial_type`, `billing_health`, `activity_level`, `buyer_role`, `email_type`, `region`, `device_type` — each takes a value from its closed vocabulary (the types enforce it). `seat_type` is not settable here; it is `assignSeatType`. The control plane applies the value before delivery, so both the browser SDK's client context and [getBuiltinDimensions](/revturbine-sdk/api/classes/revturbineserver/#getbuiltindimensions) return it: - `override: false` (default): delivered until a newer RevTurbine enrichment change supersedes it (last writer wins, enrichment included); writing a pinned dimension without the flag unpins it. - `override: true`: pinned — enrichment never replaces it until you write the dimension again without the flag, or clear it. #### Parameters ##### userId `string` The tenant's own identifier for the user (non-empty) — the same key `assignSeatType` and `createClientSession` use. ##### dimensions Dimension → value (or `null` to clear). Must name at least one dimension. ##### options? [`SetBuiltinDimensionsOptions`](/revturbine-sdk/api/interfaces/setbuiltindimensionsoptions/) = `{}` `{ override }` — see above. #### Returns `Promise`\<\{ \}\> The resulting state: `seat_type_handle`, every customer-set `builtin_dimensions` value now stored, the pinned `overrides`, and `updated_at`. #### Throws if the control plane refuses the write — `reason` is `unknown_dimension_value` for a 422 and `forbidden` for a 403 (the key is not a server key). The error never carries the key. #### Example ```ts // Your billing system knows better than RevTurbine's Stripe view: pin it. await server.setBuiltinDimensions('user_123', { subscription_state: 'paid' }, { override: true }); // Hand the dimension back to RevTurbine enrichment. await server.setBuiltinDimensions('user_123', { subscription_state: null }); ``` # SdkSession A fully-initialized SDK session with convenience methods for creating placement and entitlement controllers, updating user context, and accessing the underlying SDK instance. This is the recommended entry point for headless (non-React) consumers. ## Example ```ts const session = await initRevTurbine({ tenantId: 'tenant_abc', publicKey: 'rtk_…', endpoint: 'https://edge.example.com', mode: 'snippet', user: { id: 'user_123', plan_handle: 'pro' }, }); // Get a placement by slot ID const banner = session.placement({ surfaceSlot: { id: 'upsell_banner' } }); const decision = await banner.load(); // Check an entitlement with auto-gate const gate = session.entitlement({ handle: 'brand_kit', autoGate: true }); await gate.check(); // Update user context (works in any runtime mode) session.identify('user_456', { plan_handle: 'enterprise' }); session.setUserContext({ personalization: { company: 'Acme' } }); ``` ## Properties ### sdk > `readonly` **sdk**: [`RevTurbineCustomerSdk`](/revturbine-sdk/api/classes/revturbinecustomersdk/) The underlying SDK instance. Use for advanced/direct operations. *** ### theme > `readonly` **theme**: [`RevTurbineTheme`](/revturbine-sdk/api/interfaces/revturbinetheme/) Resolved theme. ## Methods ### can() > **can**(`handle`, `context?`): `Promise`\<\{ \}\> The entitlement check as a `can` question — alias of [checkEntitlement](/revturbine-sdk/api/classes/sdksession/#checkentitlement), promoted onto the session facade so `session.can('batch_export')` works without the `session.sdk` escape hatch (plan 179 Q-1/Q-3 ruling). Read `.allowed` for the verdict; an at-cap blocked limit resolves `limited` + `allowed: false`. #### Parameters ##### handle `string` ##### context? `RevTurbineEntitlementContext` #### Returns `Promise`\<\{ \}\> *** ### checkEntitlement() > **checkEntitlement**(`handle`, `context?`): `Promise`\<\{ \}\> One-shot entitlement check. For auto-gating or reactive updates, prefer [entitlement](/revturbine-sdk/api/classes/sdksession/#entitlement) which returns a full controller. #### Parameters ##### handle `string` ##### context? `RevTurbineEntitlementContext` #### Returns `Promise`\<\{ \}\> *** ### entitlement() > **entitlement**(`options`): `EntitlementGate` Create an EntitlementGate bound to this session's SDK. #### Parameters ##### options [`EntitlementGateOptions`](/revturbine-sdk/api/interfaces/entitlementgateoptions/) #### Returns `EntitlementGate` #### Example ```ts const gate = session.entitlement({ handle: 'brand_kit', autoGate: true }); await gate.check(); if (gate.denied) { showGate(gate.gatedPlacement); } ``` *** ### fetchUserContext() > **fetchUserContext**(`userId`): `Promise`\<`UserTargetingContext`\> Fetch full user context from the server (server runtime mode). #### Parameters ##### userId `string` #### Returns `Promise`\<`UserTargetingContext`\> *** ### flushEvents() > **flushEvents**(): `Promise`\<`void`\> Flush buffered events immediately. Headless processes are often short-lived (scripts, jobs, edge handlers) — call this before exit so buffered `track()` events aren't lost with the process. #### Returns `Promise`\<`void`\> *** ### getPlacement() > **getPlacement**(`config`): `Promise`\<\{ \} \| `null`\> Get a raw placement output by request config (slot, entitlement, plan, or chained). Returns the full PlacementOutput or `null`. #### Parameters ##### config `RevTurbinePlacementRequestConfig` #### Returns `Promise`\<\{ \} \| `null`\> *** ### getPlacementBySlotId() > **getPlacementBySlotId**(`slotId`, `options?`): `Promise`\<`RevTurbinePlacementDecision` \| `null`\> One-shot: register a surface slot, fetch a decision, and return it. For repeated use or interaction tracking, prefer [placement](/revturbine-sdk/api/classes/sdksession/#placement) which returns a full controller. `surfaceTemplateIds` limits the slot to those templates. Omitted, the slot accepts the templates the Playbook declares for `slotId`. #### Parameters ##### slotId `string` ##### options? `Omit`\<[`PlacementControllerOptions`](/revturbine-sdk/api/interfaces/placementcontrolleroptions/), `"placement"` \| `"surfaceSlot"`\> & `object` #### Returns `Promise`\<`RevTurbinePlacementDecision` \| `null`\> *** ### getTrialStatus() > **getTrialStatus**(): `Promise`\<\{ \}\> Get the current trial status. #### Returns `Promise`\<\{ \}\> *** ### getUsage() > **getUsage**(): [`RevTurbineUsageSnapshot`](/revturbine-sdk/api/type-aliases/revturbineusagesnapshot/) Get a snapshot of current usage balances. #### Returns [`RevTurbineUsageSnapshot`](/revturbine-sdk/api/type-aliases/revturbineusagesnapshot/) *** ### getUserContext() > **getUserContext**(): `object` Get the current resolved user context (includes `tenant_id`, `user_id`). #### Returns `object` *** ### identify() > **identify**(`userId`, `contextOrTraits?`): `void` Identify a user and optionally set traits/context. Triggers segment re-evaluation and clears decision cache. #### Parameters ##### userId `string` ##### contextOrTraits? `Exact`\<[`IdentifyContextInput`](/revturbine-sdk/api/type-aliases/identifycontextinput/), [`IdentifyContextInput`](/revturbine-sdk/api/type-aliases/identifycontextinput/)\> #### Returns `void` *** ### placement() > **placement**(`options`): [`PlacementController`](/revturbine-sdk/api/classes/placementcontroller/) Create a [PlacementController](/revturbine-sdk/api/classes/placementcontroller/) bound to this session's SDK. #### Parameters ##### options [`PlacementControllerOptions`](/revturbine-sdk/api/interfaces/placementcontrolleroptions/) #### Returns [`PlacementController`](/revturbine-sdk/api/classes/placementcontroller/) #### Example ```ts const banner = session.placement({ surfaceSlot: { id: 'upsell_banner' } }); await banner.load(); if (banner.visible) { renderBanner(banner.content); } ``` *** ### resetIdentity() > **resetIdentity**(): `void` Reset to anonymous user state. #### Returns `void` *** ### resetUserContext() > **resetUserContext**(): `void` Hard-reset the user context to a blank slate (no anonymous inference) — removes every user-context value plus usage balances and clears the decision cache, interaction state, and impression history. Mostly for demo / fixture flows. See [RevTurbineCustomerSdk.resetUserContext](/revturbine-sdk/api/classes/revturbinecustomersdk/#resetusercontext). #### Returns `void` *** ### setUserContext() > **setUserContext**(`context`): `void` Merge fields into the current user context. Triggers segment re-evaluation. #### Parameters ##### context [`RevTurbineUserContext`](/revturbine-sdk/api/interfaces/revturbineusercontext/) #### Returns `void` *** ### track() > **track**(`name`, `data?`): `Promise`\<`void`\> Track an event — the advertised alias of [trackEvent](/revturbine-sdk/api/classes/sdksession/#trackevent), first-class on the session facade so headless code carries the full telemetry surface (plan 179 ruling, Kent 2026-08-13). Powers analytics, frequency caps, attribution, and experiments. #### Parameters ##### name `string` ##### data? `Record`\<`string`, `JsonValue`\> #### Returns `Promise`\<`void`\> *** ### trackEvent() > **trackEvent**(`name`, `data?`): `Promise`\<`void`\> Track a custom event. #### Parameters ##### name `string` ##### data? `Record`\<`string`, `JsonValue`\> #### Returns `Promise`\<`void`\> *** ### update() > **update**(`patch`): `void` Patch the session-scoped user context — the whole-context-minus-`id` upsert (`update({ plan: {...} })`, `update({ usage: {...} })`, …). Alias of the SDK's `update()`, promoted onto the session facade so the documented `session.update()` verb is real (plan 179 Q-1/Q-3 ruling). Unrecognized keys warn (prod-visible, once per session) and drop, exactly as on the SDK — plan 191 Q-5 made the warning prod-visible rather than dev-only, so this comment was stale in the direction that matters. #### Parameters ##### patch [`RevTurbineUpdateInput`](/revturbine-sdk/api/type-aliases/revturbineupdateinput/) #### Returns `void` *** ### updateUsage() > **updateUsage**(`balances`): `void` Update usage balances (e.g. after a meter event). #### Parameters ##### balances [`UsageBalances`](/revturbine-sdk/api/type-aliases/usagebalances/) #### Returns `void` # ServerUserContextProvider A TraitsProvider keyed to `traits:server`. Auto-wired by the SDK when a client-context token is configured; call [setSnapshot](/revturbine-sdk/api/classes/serverusercontextprovider/#setsnapshot) whenever a fresh server context is fetched. ## Implements - `TraitsProvider`\<`"traits:server"`\> ## Methods ### hasSignals() > **hasSignals**(): `boolean` Whether any server signal is currently present. #### Returns `boolean` *** ### resolve() > **resolve**(): `TraitsProviderState` Resolve the current `traits:server` state for evaluation. #### Returns `TraitsProviderState` #### Implementation of `TraitsProvider.resolve` *** ### setSnapshot() > **setSnapshot**(`snapshot`): `void` Replace the server-known snapshot (called after each client-context fetch). #### Parameters ##### snapshot [`ServerUserContextSnapshot`](/revturbine-sdk/api/interfaces/serverusercontextsnapshot/) #### Returns `void` # UnsupportedExperimentAssignmentUnitError Raised before provider registration when its subject cannot serve an experiment's unit. ## Extends - `Error` ## Constructors ### Constructor > **new UnsupportedExperimentAssignmentUnitError**(`experimentHandle`, `requestedUnit`, `subjectUnit`): `UnsupportedExperimentAssignmentUnitError` Construct an unsupported-unit launch error. #### Parameters ##### experimentHandle `string` ##### requestedUnit [`NativeExperimentAssignmentUnit`](/revturbine-sdk/api/type-aliases/nativeexperimentassignmentunit/) ##### subjectUnit [`NativeExperimentAssignmentUnit`](/revturbine-sdk/api/type-aliases/nativeexperimentassignmentunit/) #### Returns `UnsupportedExperimentAssignmentUnitError` #### Overrides `Error.constructor` ## Properties ### experimentHandle > `readonly` **experimentHandle**: `string` Experiment whose requested assignment unit cannot be served. *** ### requestedUnit > `readonly` **requestedUnit**: [`NativeExperimentAssignmentUnit`](/revturbine-sdk/api/type-aliases/nativeexperimentassignmentunit/) Assignment unit requested by the canonical Experiment version. *** ### subjectUnit > `readonly` **subjectUnit**: [`NativeExperimentAssignmentUnit`](/revturbine-sdk/api/type-aliases/nativeexperimentassignmentunit/) Unit represented by the provider's configured subject. # AccessDeniedPlaceholder > **AccessDeniedPlaceholder**(`__namedParameters`): `any` Default denied state for an AccessGateSurfaceSlot (D-59, 2026-10-06): rendered when access is denied and no Access Gate placement is authored for the slot's entitlement, so a blocked user always sees that the feature is unavailable — never another entitlement's gate, never a blank. An app-supplied `deniedFallback` replaces it. ## Parameters ### \_\_namedParameters `AccessDeniedPlaceholderProps` ## Returns `any` # adaptExperimentVersionToBucketer > **adaptExperimentVersionToBucketer**(`experiment`): [`BasicBucketerExperiment`](/revturbine-sdk/api/interfaces/basicbucketerexperiment/) Translate canonical fraction weights into the bucketer's ratio + exposure representation. For weights whose positive sum is below one, the missing fraction remains unenrolled: `[0.3, 0.3]` becomes ratio `0.3:0.3` at exposure `0.6`, realizing 30% / 30% / 40% unassigned. If fractions are oversubscribed (sum above one), they are normalized proportionally within `traffic_allocation`; this keeps the conversion total for every schema-valid vector. Zero weights are omitted, matching `bucketSubject()`'s established exclusion semantics. ## Parameters ### experiment [`CanonicalExperimentAllocation`](/revturbine-sdk/api/interfaces/canonicalexperimentallocation/) ## Returns [`BasicBucketerExperiment`](/revturbine-sdk/api/interfaces/basicbucketerexperiment/) # BannerComponent > **BannerComponent**(`__namedParameters`): `any` Full-width sticky banner placement. Renders at the top or bottom of the viewport with a header, body, CTA button, and optional dismiss control. **Content fields used:** `header`, `body`, `message`, `cta_label`, `position`, `dismissible` ## Parameters ### \_\_namedParameters [`BannerComponentProps`](/revturbine-sdk/api/type-aliases/bannercomponentprops/) ## Returns `any` ## Example ```tsx navigate('/plans')} onDismiss={() => {}} visible={true} /> ``` # BannerFrame > **BannerFrame**\<`C`\>(`__namedParameters`): `any` Sticky banner component that handles: - Top/bottom position - Dismiss state + callback - Visibility reset when parent re-shows - Theme-aware default styles Extend by wrapping and injecting `children` for custom body content. ## Type Parameters ### C `C` *extends* [`ResolvedContent`](/revturbine-sdk/api/type-aliases/resolvedcontent/) = [`ResolvedContent`](/revturbine-sdk/api/type-aliases/resolvedcontent/) ## Parameters ### \_\_namedParameters `BannerFrameProps`\<`C`\> ## Returns `any` ## Example ```tsx function TrialBanner(props: PlacementSlotProps) { return ( 🔥 {props.content.days_remaining} days left in your trial ); } ``` # baseThemeForScheme > **baseThemeForScheme**(`scheme`): `Readonly`\<[`RevTurbineTheme`](/revturbine-sdk/api/interfaces/revturbinetheme/)\> The base palette for a resolved scheme. ## Parameters ### scheme `"light"` \| `"dark"` `'light'` or `'dark'`. Resolve `'system'` before calling. ## Returns `Readonly`\<[`RevTurbineTheme`](/revturbine-sdk/api/interfaces/revturbinetheme/)\> The matching base theme. # bridgeUiPathResolversIntoRegistry > **bridgeUiPathResolversIntoRegistry**(`resolvers`, `registry?`): () => `void` Bridge an SDK-init `uiPathResolvers` map into a [CtaResolverRegistry](/revturbine-sdk/api/classes/ctaresolverregistry/) so resolvers supplied at init actually dispatch on CTA clicks (plan 174 TASK-1 / F-70). Called by the SDK constructor after the init-time coverage check runs. Explicit registrations take precedence: an action type already present in the registry (via [registerCtaResolver](/revturbine-sdk/api/functions/registerctaresolver/) or a direct `register`) is left untouched, and a later explicit registration replaces the bridged entry through the registry's normal override semantics. Each bridged resolver receives the placement's raw authored `cta_path` record — the same shape init-time validation checked. A rejected async resolver is logged, never thrown into the click handler. Returns a cleanup function that unregisters exactly the entries this call bridged, skipping any the customer has since replaced. ## Parameters ### resolvers `Record`\<`string`, (`uiPath`) => `void` \| `Promise`\<`void`\>\> ### registry? [`CtaResolverRegistry`](/revturbine-sdk/api/classes/ctaresolverregistry/) = `...` ## Returns () => `void` # bucketSubject > **bucketSubject**(`subject`, `experimentHandle`, `experiment`): `string` \| `undefined` Assign one subject to one experiment, or `undefined` for not-enrolled. Exported for testing and for callers who want the decision without the provider wrapper. Pure: same inputs always give the same arm, with no storage, no clock and no randomness involved. ## Parameters ### subject `string` ### experimentHandle `string` ### experiment [`BasicBucketerExperiment`](/revturbine-sdk/api/interfaces/basicbucketerexperiment/) ## Returns `string` \| `undefined` # buildControlPlaneEvent > **buildControlPlaneEvent**(`eventType`, `payload?`): [`ControlPlaneEmitInput`](/revturbine-sdk/api/interfaces/controlplaneemitinput/) Build the `(eventName, properties)` pair for a control-plane semantic event, stamping the canonical source classification onto the payload. Pure — used both by the browser SDK's [RevTurbineCustomerSdk.trackControlPlaneEvent](/revturbine-sdk/api/classes/revturbinecustomersdk/#trackcontrolplaneevent) and by server/CLI emitters that POST the same shape to the ingest endpoint. ## Parameters ### eventType `"web_signed_up"` \| `"web_signed_in"` \| `"cli_signed_up"` \| `"cli_signed_in"` \| `"cli_command_executed"` \| `"playbook_version_submitted"` \| `"playbook_version_approved"` \| `"playbook_version_rejected"` \| `"playbook_version_deployed"` \| `"playbook_version_launched"` \| `"playbook_version_parked"` \| `"playbook_version_resumed"` \| `"playbook_version_discarded"` \| `"playbook_version_archived"` \| `"playbook_imported"` \| `"playbook_exported"` \| `"entity_created"` \| `"entity_updated"` \| `"entity_deleted"` \| `"web_api_error"` A canonical control-plane event type. ### payload? `Record`\<`string`, `JsonValue`\> = `{}` Optional event-specific properties (e.g. `{ resource, resource_id }`). ## Returns [`ControlPlaneEmitInput`](/revturbine-sdk/api/interfaces/controlplaneemitinput/) The emit input to hand to the SDK's `capture` path. # ButtonComponent > **ButtonComponent**(`__namedParameters`): `any` Standalone CTA button placement. Renders a single styled button suitable for persistent nav CTAs or inline upgrade prompts. **Content fields used:** `cta_label`, `style` ## Parameters ### \_\_namedParameters [`ButtonComponentProps`](/revturbine-sdk/api/type-aliases/buttoncomponentprops/) ## Returns `any` # categorizeActionError > **categorizeActionError**(`err`): [`ActionErrorCategory`](/revturbine-sdk/api/type-aliases/actionerrorcategory/) Bucket an unknown thrown value into a [ActionErrorCategory](/revturbine-sdk/api/type-aliases/actionerrorcategory/) without surfacing its message (plan 144 TASK-14 — non-sensitive categories only). ## Parameters ### err `unknown` ## Returns [`ActionErrorCategory`](/revturbine-sdk/api/type-aliases/actionerrorcategory/) # clearPersistedTheme > **clearPersistedTheme**(`tenantId`, `storage?`): `void` Clear persisted theme (useful on identity reset). ## Parameters ### tenantId `string` ### storage? `RevTurbineStorage` ## Returns `void` # collectAssignmentFactDeclarations > **collectAssignmentFactDeclarations**(`sources`, `explicit?`): `ReadonlyMap`\<`string`, [`ExperimentAssignmentFactDeclaration`](/revturbine-sdk/api/interfaces/experimentassignmentfactdeclaration/)\> Harvest valid assignment-fact declarations from providers exposing the [ExperimentAssignmentFactMetadataCarrier](/revturbine-sdk/api/interfaces/experimentassignmentfactmetadatacarrier/) surface. Later sources win on a duplicate handle, so pass the most authoritative source last. ## Parameters ### sources readonly (`object` \| `undefined`)[] ### explicit? readonly [`ExperimentAssignmentFactDeclaration`](/revturbine-sdk/api/interfaces/experimentassignmentfactdeclaration/)[] ## Returns `ReadonlyMap`\<`string`, [`ExperimentAssignmentFactDeclaration`](/revturbine-sdk/api/interfaces/experimentassignmentfactdeclaration/)\> # CompactCircularGauge > **CompactCircularGauge**(`__namedParameters`): `any` Minimal circular gauge for inline usage indicators. Can be used standalone or as part of a `QuotaMeterFrame`. ## Parameters ### \_\_namedParameters `CompactCircularGaugeProps` ## Returns `any` # composeEffectiveExperimentContext > **composeEffectiveExperimentContext**\<`TContext`\>(`callerContext`, `providerState`, `contextRevision`): `EffectiveUserContextResolution` & `object` Materialize one ephemeral user context from caller context and normalized provider outcomes. The caller-owned object is never mutated. ## Type Parameters ### TContext `TContext` *extends* `UserContextSnapshot` ## Parameters ### callerContext `Readonly`\<`TContext`\> ### providerState `ExperimentProviderState` \| `undefined` ### contextRevision `string` ## Returns `EffectiveUserContextResolution` & `object` # createAnalyticsProvider > **createAnalyticsProvider**(`options`): `EventConsumerProvider` Create an analytics domain provider that forwards SDK events (impressions, interactions, triggers, page views, etc.) to a third-party analytics platform. The returned provider should be passed in the `domainProviders` array when calling `initRevTurbine()`. ## Parameters ### options [`AnalyticsProviderOptions`](/revturbine-sdk/api/interfaces/analyticsprovideroptions/) ## Returns `EventConsumerProvider` ## Examples ```ts const analytics = createAnalyticsProvider({ handler: (eventName, properties) => { window.analytics.track(eventName, properties); }, }); ``` ```ts const analytics = createAnalyticsProvider({ handler: (eventName, properties) => { heap.track(eventName, properties); }, filter: ['placement_interaction', 'placement_dismissed', 'placement_converted'], }); ``` ```ts const analytics = createAnalyticsProvider({ handler: (name, props) => posthog.capture(name, props), transform: (name, props) => ({ eventName: `revturbine.${name}`, properties: { ...props, source: 'revturbine-sdk' }, }), }); ``` # createBasicExperimentProvider > **createBasicExperimentProvider**(`options`): `ExperimentAssignmentProvider` Build the built-in bucketer. Register it explicitly to switch it on: ```ts initRevTurbine({ domainProviders: [ createBasicExperimentProvider({ subject: () => currentUserId, experiments: { pricing_test: { variants: ['control', 'variant_b'] } }, }), ], }); ``` ## Parameters ### options `BasicBucketerOptions` ## Returns `ExperimentAssignmentProvider` # createChainedPlacementRequest > **createChainedPlacementRequest**(`placementHandle`, `options?`): `RevTurbinePlacementRequestConfig` Create a typed placement request for chained/CTA follow-up lookups. ## Parameters ### placementHandle `string` ### options? [`RevTurbineChainedPlacementRequestOptions`](/revturbine-sdk/api/interfaces/revturbinechainedplacementrequestoptions/) = `{}` ## Returns `RevTurbinePlacementRequestConfig` # createCompositeExperimentProvider > **createCompositeExperimentProvider**(`providers`, `options?`): `ExperimentAssignmentProvider` Combine customer-owned experiment adapters behind the registry's single `experiments` domain. Overlapping declared ownership is rejected for the affected handles instead of allowing registration order to choose a winner. ## Parameters ### providers readonly `ExperimentAssignmentProvider`[] ### options? [`CompositeExperimentProviderOptions`](/revturbine-sdk/api/interfaces/compositeexperimentprovideroptions/) = `{}` ## Returns `ExperimentAssignmentProvider` # createCustomEndpointRuntimeConfig > **createCustomEndpointRuntimeConfig**(`options`): `RevTurbineInitOptions` Build a custom-endpoint SDK config. ## Parameters ### options [`RevTurbineInitBaseOptions`](/revturbine-sdk/api/type-aliases/revturbineinitbaseoptions/) & `object` ## Returns `RevTurbineInitOptions` # createEntitlementPlacementRequest > **createEntitlementPlacementRequest**(`entitlementHandle`, `options?`): `RevTurbinePlacementRequestConfig` Create a typed placement request for entitlement-gated lookups. ## Parameters ### entitlementHandle `string` ### options? [`RevTurbineEntitlementPlacementRequestOptions`](/revturbine-sdk/api/interfaces/revturbineentitlementplacementrequestoptions/) = `{}` ## Returns `RevTurbinePlacementRequestConfig` # createLocalRuntimeConfig ## Call Signature > **createLocalRuntimeConfig**\<`TUiPaths`\>(`options`): `RevTurbineInitOptions` Build a local-only SDK config. ### Type Parameters #### TUiPaths `TUiPaths` *extends* readonly `unknown`[] ### Parameters #### options [`RevTurbineInitBaseOptions`](/revturbine-sdk/api/type-aliases/revturbineinitbaseoptions/) & `object` ### Returns `RevTurbineInitOptions` ## Call Signature > **createLocalRuntimeConfig**(`options`): `RevTurbineInitOptions` Build a local-only SDK config. ### Parameters #### options [`RevTurbineInitBaseOptions`](/revturbine-sdk/api/type-aliases/revturbineinitbaseoptions/) & `object` ### Returns `RevTurbineInitOptions` # createNativeExperimentAssignmentProvider > **createNativeExperimentAssignmentProvider**(`options`): `ExperimentAssignmentProvider` & [`ExperimentAssignmentFactMetadataCarrier`](/revturbine-sdk/api/interfaces/experimentassignmentfactmetadatacarrier/) Build the native assignment provider whose allocation authority is the canonical Experiment version rather than duplicated SDK configuration. ## Parameters ### options [`NativeExperimentAssignmentOptions`](/revturbine-sdk/api/interfaces/nativeexperimentassignmentoptions/) ## Returns `ExperimentAssignmentProvider` & [`ExperimentAssignmentFactMetadataCarrier`](/revturbine-sdk/api/interfaces/experimentassignmentfactmetadatacarrier/) # createPostHogAnalyticsProvider > **createPostHogAnalyticsProvider**(`options`): `EventConsumerProvider` Create an analytics resolver that forwards SDK events to PostHog (plan 112). A thin PostHog-typed convenience over [createAnalyticsProvider](/revturbine-sdk/api/functions/createanalyticsprovider/): each SDK event becomes `posthog.capture(eventName, properties)`. Pass the result in the `domainProviders` array at init so the same semantic events feed both the first-party ingest pipeline and PostHog. Like [createAnalyticsProvider](/revturbine-sdk/api/functions/createanalyticsprovider/), a throw from `posthog.capture` is swallowed — a PostHog failure never crashes the SDK event pipeline. ## Parameters ### options [`PostHogAnalyticsProviderOptions`](/revturbine-sdk/api/interfaces/posthoganalyticsprovideroptions/) ## Returns `EventConsumerProvider` ## Example ```ts import posthog from 'posthog-js'; import { createPostHogAnalyticsProvider, initRevTurbine } from '@revturbine/sdk'; const analytics = createPostHogAnalyticsProvider({ posthog }); const session = await initRevTurbine({ tenantId: 'tenant_abc', publicKey: 'rtk_…', endpoint: 'https://edge.example.com', mode: 'snippet', domainProviders: [analytics], }); ``` # createPostHogIntegration > **createPostHogIntegration**(`options`): `EventConsumerProvider` Create a PostHog integration with an optional identity lifecycle (plan 144 TASK-17 / REQ-26). A superset of [createPostHogAnalyticsProvider](/revturbine-sdk/api/functions/createposthoganalyticsprovider/): every event is still `posthog.capture(name, properties)` with the **same `filter` / `transform` semantics** (REQ-27), and — when the matching flag is on — RevTurbine's identify/logout, account grouping, and navigation are mirrored to PostHog's native `identify` / `reset` / `group` / `$pageview`. All sync flags default `false`, so by default this behaves exactly like the capture-only provider. No PostHog package is imported or bundled — the client is injected — so this code is tree-shakable and only pulled in when you import it (AC-20). A throw from any PostHog call is swallowed and never blocks RevTurbine ingest (AC-17). `createPostHogAnalyticsProvider` remains exported for existing integrations. ## Parameters ### options [`PostHogIntegrationOptions`](/revturbine-sdk/api/interfaces/posthogintegrationoptions/) ## Returns `EventConsumerProvider` # createRevTurbineApiClient > **createRevTurbineApiClient**(`config`): `Client` Create a configured RevTurbine API client instance. ## Parameters ### config `RevTurbineApiClientConfig` Client configuration. ## Returns `Client` Typed API client with GET, POST, PUT, PATCH, DELETE methods. # createSemanticEvent > **createSemanticEvent**(`eventType`, `payload`, `options?`): [`RevTurbineSemanticEvent`](/revturbine-sdk/api/interfaces/revturbinesemanticevent/) Create a typed semantic event payload. ## Parameters ### eventType `string` ### payload [`SdkEventProperties`](/revturbine-sdk/api/type-aliases/sdkeventproperties/) ### options? `RevTurbineEventOptions` ## Returns [`RevTurbineSemanticEvent`](/revturbine-sdk/api/interfaces/revturbinesemanticevent/) # createServerRuntimeConfig > **createServerRuntimeConfig**(`options`): `RevTurbineInitOptions` Build a server-backed SDK config (default runtime mode). ## Parameters ### options [`RevTurbineInitBaseOptions`](/revturbine-sdk/api/type-aliases/revturbineinitbaseoptions/) ## Returns `RevTurbineInitOptions` # createSlotPlacementRequest > **createSlotPlacementRequest**(`slotId`, `componentType`, `options?`): `RevTurbinePlacementRequestConfig` Create a typed placement request for slot + surface lookups. ## Parameters ### slotId `string` ### componentType `"inline"` \| `"custom"` \| `"push"` \| `"banner"` \| `"modal"` \| `"tooltip"` \| `"sidebar"` \| `"toast"` \| `"fullscreen"` \| `"email"` \| `"sms"` \| `"in_page"` \| `"button"` \| `"full_page"` \| `"agent"` \| `"cli"` ### options? [`RevTurbineSlotPlacementRequestOptions`](/revturbine-sdk/api/interfaces/revturbineslotplacementrequestoptions/) = `{}` ## Returns `RevTurbinePlacementRequestConfig` # createStrictLocalRuntimeConfig > **createStrictLocalRuntimeConfig**\<`TUiPaths`\>(`options`): `RevTurbineInitOptions` Build a strict local-only SDK config. Unlike [createLocalRuntimeConfig](/revturbine-sdk/api/functions/createlocalruntimeconfig/), this helper always requires `exportedConfig.content_ui_paths` and complete `uiPathResolvers` coverage at compile time for action types present in that Playbook. ## Type Parameters ### TUiPaths `TUiPaths` *extends* readonly `unknown`[] ## Parameters ### options [`RevTurbineInitBaseOptions`](/revturbine-sdk/api/type-aliases/revturbineinitbaseoptions/) & `object` ## Returns `RevTurbineInitOptions` # createTreatmentInteraction > **createTreatmentInteraction**(`userId`, `placementId`, `interactionType`, `options?`): `RevTurbineTreatmentInteractionInput` Create a typed treatment interaction payload. ## Parameters ### userId `string` ### placementId `string` ### interactionType `RevTurbineTreatmentInteractionType` ### options? [`RevTurbineTreatmentInteractionOptions`](/revturbine-sdk/api/interfaces/revturbinetreatmentinteractionoptions/) = `{}` ## Returns `RevTurbineTreatmentInteractionInput` # currentBrowserRoute > **currentBrowserRoute**(): `string` \| `null` The browser's current route — `window.location.pathname`, normalized. `null` outside a browser (SSR, server runtimes, workers without `location`). ## Returns `string` \| `null` # defineUiPathResolvers > **defineUiPathResolvers**\<`TUiPaths`\>(`uiPaths`, `resolvers`): [`RevTurbineUiPathResolverMap`](/revturbine-sdk/api/type-aliases/revturbineuipathresolvermap/) Compile-time helper for authoring complete UI-path resolver maps. When `uiPaths` is provided as a `const` array, TypeScript enforces that `resolvers` contains every `action_type` key. ## Type Parameters ### TUiPaths `TUiPaths` *extends* readonly `unknown`[] ## Parameters ### uiPaths `TUiPaths` ### resolvers [`RevTurbineRequiredUiPathResolvers`](/revturbine-sdk/api/type-aliases/revturbinerequireduipathresolvers/)\<`TUiPaths`\> & [`RevTurbineUiPathResolverMap`](/revturbine-sdk/api/type-aliases/revturbineuipathresolvermap/) ## Returns [`RevTurbineUiPathResolverMap`](/revturbine-sdk/api/type-aliases/revturbineuipathresolvermap/) # dispatchCtaClick > **dispatchCtaClick**(`uiPath`, `context`, `resolvers`, `fallback?`): `boolean` Dispatch a CTA activation: invoke the registered resolver for the action type if one exists, otherwise fall back to the supplied callback. Returns `true` when a registered resolver handled the click, `false` when it fell through to `fallback`. A registered resolver fully handles the action — the fallback is not also called. ## Parameters ### uiPath [`PlacementUiPath`](/revturbine-sdk/api/interfaces/placementuipath/) ### context [`CtaResolverContext`](/revturbine-sdk/api/interfaces/ctaresolvercontext/) ### resolvers [`CtaResolverRegistry`](/revturbine-sdk/api/classes/ctaresolverregistry/) ### fallback? (`uiPath`) => `void` ## Returns `boolean` # EmailPreviewComponent > **EmailPreviewComponent**(`__namedParameters`): `any` Static email preview: envelope card with a subject header and body, plus the CTA row. **Content fields used:** `subject`, `body`, `cta_label`, `secondary_cta_label` ## Parameters ### \_\_namedParameters [`PlacementSlotProps`](/revturbine-sdk/api/interfaces/placementslotprops/) ## Returns `any` # EngagementArea > **EngagementArea**(`__namedParameters`): `ReactElement` A qualified engagement region (plan 144 TASK-13 / REQ-21). It: - establishes a [TelemetryScope](/revturbine-sdk/api/functions/telemetryscope/) (`area` / `purpose`) for its descendants; - emits `viewEvent` **once** when first viewport-qualified — one-shot across React Strict Mode's double-mount (AC-12); - accrues dwell only while the region is **onscreen and the tab is visible**, emitting `dwellEvent` with `dwell_ms` when it unmounts; - bubbles descendant clicks as `interactionEvent`. Renders its children and never throws without a provider (AC-13). ## Parameters ### \_\_namedParameters [`EngagementAreaProps`](/revturbine-sdk/api/interfaces/engagementareaprops/) ## Returns `ReactElement` # entitlementResultDenies > **entitlementResultDenies**(`result`): `boolean` Whether an entitlement result denies access. A result denies when its `status` is `'denied'` OR the evaluator's `allowed` verdict is explicitly `false`. The second clause matters for at-cap limit rules: blocking enforcement — including the unset-enforcement default — resolves `{ status: 'limited', allowed: false }`, and gating on status alone silently granted access at the cap (plan 179 TASK-10; the cold-funnel "cap never blocks" trap). Degrade mode (`limited` + `allowed: true`) stays granted. ## Parameters ### result \{ \} \| `null` ## Returns `boolean` # fallbackAccountId > **fallbackAccountId**(`userId`): `string` Build the fallback account key for a user id — [FALLBACK\_ACCOUNT\_ID\_PREFIX](/revturbine-sdk/api/variables/fallback_account_id_prefix/) followed by the id verbatim. The id is used exactly as given: redaction and trimming belong to the caller, which already resolved the `user_id` it is about to put on the same wire row. Deriving the key here from a *different* normalization is how the two lanes would stop joining. ## Parameters ### userId `string` The user id (or anonymous id) the event is attributed to, already normalized/redacted exactly as it will appear in `user_id`. ## Returns `string` The prefixed fallback account key. # FixedSurfaceSlot > **FixedSurfaceSlot**(`__namedParameters`): `any` Fixed surface slot — always renders an embedded placement. Use for inline placements that should always be present on the page: upgrade banners, quota meters, promo cards, embedded CTAs, etc. The slot loads automatically on mount. If no placement matches, `fallback` is rendered so the page never has an empty gap. ## Parameters ### \_\_namedParameters `FixedSurfaceSlotProps` ## Returns `any` ## Example ```tsx } /> ``` # formatCurrencyMinorUnits > **formatCurrencyMinorUnits**(`amount`, `currencyCode`, `locale?`): `string` Format an integer minor-unit amount for Playbook-backed display tokens. ## Parameters ### amount `number` ### currencyCode `string` ### locale? `string` ## Returns `string` # Gate > **Gate**(`__namedParameters`): `any` Access-gate surface slot — renders children when entitled (allowed *or* `limited`), or a gated placement when access is `denied`. Checks entitlements and/or usage thresholds. On denial, displays the configured gated placement (or `deniedFallback` if no placement matches). On success, renders `children` — or `limitedFallback` when the entitlement is `limited`. ## Parameters ### \_\_namedParameters `AccessGateSurfaceSlotProps` ## Returns `any` ## Example ```tsx // `can` shorthand — mirrors useCan('mp4_download') Upgrade to export}> // soft-warn while still granting access when the balance runs low }> } > ``` # getDefaultCtaResolverRegistry > **getDefaultCtaResolverRegistry**(): [`CtaResolverRegistry`](/revturbine-sdk/api/classes/ctaresolverregistry/) Get or create the default global CTA resolver registry. The [PlacementRenderer](/revturbine-sdk/api/functions/placementrenderer/) uses this when no `ctaResolvers` prop is supplied. ## Returns [`CtaResolverRegistry`](/revturbine-sdk/api/classes/ctaresolverregistry/) # getDefaultRegistry > **getDefaultRegistry**(): [`PlacementTypeRegistry`](/revturbine-sdk/api/classes/placementtyperegistry/) Get or create the default global placement type registry. On the React entry the registry comes pre-seeded with the built-in slot types; on the headless entry it starts empty (headless consumers register their own types via [PlacementTypeRegistry.register](/revturbine-sdk/api/classes/placementtyperegistry/#register)). ## Returns [`PlacementTypeRegistry`](/revturbine-sdk/api/classes/placementtyperegistry/) # InitFailureDiagnostic > **InitFailureDiagnostic**(`__namedParameters`): `any` Render a development-only banner describing a failed SDK initialization. Returns `null` for a healthy status, so the caller can render it unconditionally in development without branching. ## Parameters ### \_\_namedParameters [`InitFailureDiagnosticProps`](/revturbine-sdk/api/interfaces/initfailurediagnosticprops/) ## Returns `any` # initRevTurbine > **initRevTurbine**\<`TUser`\>(`options`): `Promise`\<[`SdkSession`](/revturbine-sdk/api/classes/sdksession/)\> Create a fully-initialized SDK session. This is the recommended entry point for headless (non-React) consumers. It handles: 1. SDK initialization via `initRevTurbine()` 2. User identification (if `options.user.id` is provided) 3. Theme resolution (from RevTurbineConfig or API) 4. Placement bootstrapping (preloading decisions) The returned [SdkSession](/revturbine-sdk/api/classes/sdksession/) exposes the full imperative API: user context management, placement controllers, entitlement gates, and event tracking. ## Type Parameters ### TUser `TUser` *extends* [`RevTurbineUserContext`](/revturbine-sdk/api/interfaces/revturbineusercontext/) = [`RevTurbineUserContext`](/revturbine-sdk/api/interfaces/revturbineusercontext/) ## Parameters ### options [`SdkSessionOptions`](/revturbine-sdk/api/type-aliases/sdksessionoptions/) & `object` ## Returns `Promise`\<[`SdkSession`](/revturbine-sdk/api/classes/sdksession/)\> ## Example ```ts const session = await initRevTurbine({ tenantId: 'tenant_abc', publicKey: 'rtk_…', endpoint: 'https://edge.example.com', mode: 'snippet', user: { id: 'user_123', plan_handle: 'pro' }, bootstrapPlacements: [ { placement: { name: 'pricing_banner' } }, ], }); ``` # initStatusForError > **initStatusForError**(`phase`, `error`): [`RevTurbineInitStatus`](/revturbine-sdk/api/interfaces/revturbineinitstatus/) Build the failure status for a thrown init error. ## Parameters ### phase [`RevTurbineInitPhase`](/revturbine-sdk/api/type-aliases/revturbineinitphase/) The phase that was running when the error was thrown. ### error `unknown` The thrown value; non-Errors are stringified. ## Returns [`RevTurbineInitStatus`](/revturbine-sdk/api/interfaces/revturbineinitstatus/) A fully-populated failure status. # InlineCardPlacement > **InlineCardPlacement**\<`C`\>(`__namedParameters`): `any` Abstract inline card/embed placement rendered within the page flow. Handles: - Visibility gating - Theme-aware card container with border, radius, padding - Optional image, header, body, and CTA - `data-rt-placement="inline-embed"` attribute Extend by wrapping and injecting `children` for custom card bodies. ## Type Parameters ### C `C` *extends* [`ResolvedContent`](/revturbine-sdk/api/type-aliases/resolvedcontent/) = [`ResolvedContent`](/revturbine-sdk/api/type-aliases/resolvedcontent/) ## Parameters ### \_\_namedParameters `InlineCardPlacementProps`\<`C`\> ## Returns `any` ## Example ```tsx function FeatureCallout(props: PlacementSlotProps) { return ( ); } ``` # InlineEmbedComponent > **InlineEmbedComponent**(`__namedParameters`): `any` Inline card/embed placement rendered within the page flow. Lightweight card with optional image, header, body, and CTA button. Designed to sit naturally within page content. **Content fields used:** `header`, `body`, `message`, `image_url`, `cta_label` ## Parameters ### \_\_namedParameters [`InlineEmbedComponentProps`](/revturbine-sdk/api/type-aliases/inlineembedcomponentprops/) ## Returns `any` # isExperimentAssignmentFactDeclaration > **isExperimentAssignmentFactDeclaration**(`value`): `value is ExperimentAssignmentFactDeclaration` Defensively validate one declaration from a plain-JS caller. The SDK is exposed as `window.RevTurbine`, so shapes are checked, never assumed. ## Parameters ### value `unknown` ## Returns `value is ExperimentAssignmentFactDeclaration` # isFallbackAccountId > **isFallbackAccountId**(`accountId`): `boolean` Whether an `account_id` is one the SDK fabricated from a user id rather than one the integration identified. Use it to keep fabricated keys out of account denominators, exactly as the warehouse read-time guards do. `null`, `undefined`, and the bare prefix with nothing after it are **not** fallback keys — the first two are absence, and the third is not a key at all. ## Parameters ### accountId `string` \| `null` \| `undefined` The account id to classify. ## Returns `boolean` `true` only for a non-empty id carrying the fallback prefix. # isIdentifierLikeSegment > **isIdentifierLikeSegment**(`segment`): `boolean` `true` when a single path segment looks like an identifier or personal data rather than a stable route name. Exported for tests and custom resolvers. ## Parameters ### segment `string` ## Returns `boolean` # loadTheme > **loadTheme**(`opts`, `onUpdate?`): `Promise`\<[`RevTurbineTheme`](/revturbine-sdk/api/interfaces/revturbinetheme/)\> Load the tenant's theme: 1. Return the locally-persisted theme immediately (fast path). 2. Fetch the latest from the API in the background. 3. If the remote version differs, update local storage and return the new theme. Returns a resolved [RevTurbineTheme](/revturbine-sdk/api/interfaces/revturbinetheme/) (merged with defaults). Also provides an `onUpdate` callback callers can use to react to background refreshes. ## Parameters ### opts `ThemeLoaderOptions` ### onUpdate? (`theme`) => `void` ## Returns `Promise`\<[`RevTurbineTheme`](/revturbine-sdk/api/interfaces/revturbinetheme/)\> # mergeTheme > **mergeTheme**(`input?`, `base?`): [`RevTurbineTheme`](/revturbine-sdk/api/interfaces/revturbinetheme/) Deep-merge a partial theme input with a base theme, producing a complete [RevTurbineTheme](/revturbine-sdk/api/interfaces/revturbinetheme/). ## Parameters ### input? [`RevTurbineThemeInput`](/revturbine-sdk/api/type-aliases/revturbinethemeinput/) \| `null` Partial tokens. Every omitted token falls back to `base`. ### base? `Readonly`\<[`RevTurbineTheme`](/revturbine-sdk/api/interfaces/revturbinetheme/)\> = `DEFAULT_THEME` The palette to merge over. Defaults to [DEFAULT\_THEME](/revturbine-sdk/api/variables/default_theme/); pass [DARK\_THEME](/revturbine-sdk/api/variables/dark_theme/) (or use [baseThemeForScheme](/revturbine-sdk/api/functions/basethemeforscheme/)) to resolve a partial brand theme against the dark palette instead. ## Returns [`RevTurbineTheme`](/revturbine-sdk/api/interfaces/revturbinetheme/) # ModalComponent > **ModalComponent**(`__namedParameters`): `any` Centered modal overlay placement. Supports optional and blocking modes. Optional modals can be dismissed via ESC key, close button, or overlay click. Blocking modals require CTA interaction. **Content fields used:** `header`, `body`, `image_url`, `cta_label`, `secondary_cta_label`, `style` **Accessibility:** `role="dialog"`, `aria-modal="true"`, keyboard trap ## Parameters ### \_\_namedParameters [`ModalComponentProps`](/revturbine-sdk/api/type-aliases/modalcomponentprops/) ## Returns `any` # ModalFrame > **ModalFrame**\<`C`\>(`__namedParameters`): `any` Modal overlay component that handles: - Backdrop click dismiss (optional mode) - ESC key dismiss (optional mode) - Focus trap on the dialog - Visibility + dismissed state management - Theme-aware default styles Extend by wrapping and injecting `children` for custom body content. ## Type Parameters ### C `C` *extends* [`ResolvedContent`](/revturbine-sdk/api/type-aliases/resolvedcontent/) = [`ResolvedContent`](/revturbine-sdk/api/type-aliases/resolvedcontent/) ## Parameters ### \_\_namedParameters `ModalFrameProps`\<`C`\> ## Returns `any` ## Example ```tsx function MyModal(props: PlacementSlotProps) { return ( ); } ``` # normalizeConfigArtifactOrThrow > **normalizeConfigArtifactOrThrow**(`raw`, `source`, `targetDefaults?`): \{ \} \| `undefined` Parse either supported wire shape into the canonical Playbook shape. Presence of either canonical discriminator selects the canonical parser, so an unsupported future `format_version` can never fall back to legacy. ## Parameters ### raw `unknown` ### source `string` ### targetDefaults? [`ConfigTargetDefaults`](/revturbine-sdk/api/interfaces/configtargetdefaults/) ## Returns \{ \} \| `undefined` # normalizeSlotRoute > **normalizeSlotRoute**(`input`): `string` \| `null` Normalize a path, URL or route pattern into a slot route. Strips any origin, query string and fragment; collapses duplicate slashes and a trailing slash; keeps route-pattern segments (`[id]`, `:id`) verbatim; templates identifier-like segments to `:id`; bounds the result to [SLOT\_ROUTE\_MAX\_LENGTH](/revturbine-sdk/api/variables/slot_route_max_length/). Returns `null` for empty or non-string input. ## Parameters ### input `string` \| `null` \| `undefined` ## Returns `string` \| `null` ## Example ```ts normalizeSlotRoute('https://app.example.com/accounts/4821/billing?tab=x#top'); // → '/accounts/:id/billing' normalizeSlotRoute('/projects/[projectId]'); // → '/projects/[projectId]' ``` # parsePromotion > **parsePromotion**(`raw?`): \{ \} \| `undefined` Parse a promotion record from a placement output. ## Parameters ### raw? `Record`\<`string`, `unknown`\> ## Returns \{ \} \| `undefined` # parseUiPath > **parseUiPath**(`raw`): [`PlacementUiPath`](/revturbine-sdk/api/interfaces/placementuipath/) Parse a `cta_path` (or legacy `ui_path`) record from a placement output into a typed [PlacementUiPath](/revturbine-sdk/api/interfaces/placementuipath/). Built-in fields (`url`, `plan_handle`, …) are lifted onto typed properties. The `type` is preserved verbatim for any non-empty string — including tenant-defined custom action names — so a registered [CtaResolver](/revturbine-sdk/api/type-aliases/ctaresolver/) can key on it; only an absent or non-string `type` defaults to `'dismiss'`. Every remaining key is collected into `params` for custom resolvers to read. ## Parameters ### raw `Record`\<`string`, `unknown`\> ## Returns [`PlacementUiPath`](/revturbine-sdk/api/interfaces/placementuipath/) # Placement > **Placement**(`__namedParameters`): `any` Render-prop component for placement integration. An alternative to [usePlacement](/revturbine-sdk/api/functions/useplacement/) for class components or when render-prop composition is preferred. ## Parameters ### \_\_namedParameters [`PlacementProps`](/revturbine-sdk/api/type-aliases/placementprops/) ## Returns `any` ## Example ```tsx {({ visible, content, ctaClick }) => visible ? (

{content?.header}

) : null }
``` # PlacementDecisionInspector > **PlacementDecisionInspector**(`__namedParameters`): `any` Visual debug panel that explains why a placement decision was selected. Includes segment predicate evaluations, entitlement-rule matching, final decision metadata, and policy/targeting snapshots. ## Parameters ### \_\_namedParameters [`PlacementDecisionInspectorProps`](/revturbine-sdk/api/interfaces/placementdecisioninspectorprops/) ## Returns `any` # PlacementRenderer > **PlacementRenderer**(`__namedParameters`): `any` Resolves a placement output to the appropriate registered slot type and renders it with resolved content. This is the primary component customers use to render placements. It handles: - Slot type resolution from the registry - Personalization token expansion - UI path and promotion parsing - CTA and dismiss callback wiring ## Parameters ### \_\_namedParameters `PlacementRendererProps` ## Returns `any` # PushPreviewComponent > **PushPreviewComponent**(`__namedParameters`): `any` Static push-notification preview: a notification card with a title and body, plus the CTA row. **Content fields used:** `header`/`title`, `body`, `cta_label`, `secondary_cta_label` ## Parameters ### \_\_namedParameters [`PlacementSlotProps`](/revturbine-sdk/api/interfaces/placementslotprops/) ## Returns `any` # QuotaMeterFrame > **QuotaMeterFrame**\<`C`\>(`__namedParameters`): `any` Quota/usage meter with multiple display variants. Handles: - Progress bar, circular gauge, or numeric counter rendering - Threshold-based visibility (`showAtPercent`) - Typed usage props override content fields - `data-rt-placement="quota-meter"` attribute Extend by wrapping and injecting `children` for custom content below the meter. ## Type Parameters ### C `C` *extends* [`ResolvedContent`](/revturbine-sdk/api/type-aliases/resolvedcontent/) = [`ResolvedContent`](/revturbine-sdk/api/type-aliases/resolvedcontent/) ## Parameters ### \_\_namedParameters `QuotaMeterFrameProps`\<`C`\> ## Returns `any` ## Example ```tsx function MinutesMeter(props: PlacementSlotProps) { const { minutesUsed, minutesLimit } = useUsage(); return ( ); } ``` # registerBuiltinSlotTypes > **registerBuiltinSlotTypes**(`registry`): `void` Register all built-in slot types on the given registry. Built-in types cover the core surface types: - banner → BannerSlot (full-width top/bottom) - modal → ModalSlot (overlay dialog, optional/blocking) - in_page → InPageSlot (card/embed in page flow) - toast → ToastSlot (ephemeral notification) - button → ButtonSlot (nav bar / CTA button) - full_page → FullPageSlot (dedicated managed page) - email/sms/push → channel previews (static out-of-band mocks) Additional specialized types registered as in_page variants: - quota_meter → QuotaMeterSlot (usage meter + upgrade CTA) ## Parameters ### registry [`PlacementTypeRegistry`](/revturbine-sdk/api/classes/placementtyperegistry/) ## Returns `void` # registerBuiltinSnoozeResolver > **registerBuiltinSnoozeResolver**(`snooze`, `registry?`): () => `void` Register the built-in `snooze` CTA resolver (plan 174 TASK-6 companion / Q-5): an authored `snooze` ui-path dispatches to the SDK's remind-later verb, honoring a payload-authored `remind_later_minutes` window when the placement output carries one (plan 167; the verb's default matches the 60-minute tenant default). Called by the SDK constructor AFTER the init `uiPathResolvers` bridge, and skips registration when a `snooze` resolver already exists — so a customer's resolver (init map or explicit registration, before or after init) always wins. Returns a cleanup that unregisters only this entry. ## Parameters ### snooze (`outputId`, `seconds?`) => `void` ### registry? [`CtaResolverRegistry`](/revturbine-sdk/api/classes/ctaresolverregistry/) = `...` ## Returns () => `void` # registerCtaResolver > **registerCtaResolver**(`type`, `resolver`): `void` Register a [CtaResolver](/revturbine-sdk/api/type-aliases/ctaresolver/) on the default global registry. This is the ergonomic entry point for customers: ```ts registerCtaResolver('connect_crm', (uiPath) => { openCrmModal(uiPath.url, uiPath.params); }); ``` ## Parameters ### type `string` ### resolver [`CtaResolver`](/revturbine-sdk/api/type-aliases/ctaresolver/) ## Returns `void` # registerServerActionResolvers > **registerServerActionResolvers**(`actions`, `callbacks`, `registry?`): () => `void` Register app-owned server actions behind the existing CTA registry. Called after the init `uiPathResolvers` bridge and built-ins, so any resolver the customer registered through those surfaces wins. A later explicit `registerCtaResolver()` still replaces this entry normally. Handler failures are contained, reported, and tracked without applying context. ## Parameters ### actions [`ServerActionMap`](/revturbine-sdk/api/type-aliases/serveractionmap/) ### callbacks `ServerActionResolverCallbacks` ### registry? [`CtaResolverRegistry`](/revturbine-sdk/api/classes/ctaresolverregistry/) = `...` ## Returns () => `void` # remediationFor > **remediationFor**(`message`): `string` Resolve the remediation for a thrown init error. ## Parameters ### message `string` The error message as thrown. ## Returns `string` Actionable guidance. Never empty. # resetDefaultCtaResolverRegistry > **resetDefaultCtaResolverRegistry**(): `void` Reset the default CTA resolver registry (useful for testing and for tearing down tenant-specific resolvers). ## Returns `void` # resetDefaultRegistry > **resetDefaultRegistry**(): `void` Reset the default registry (useful for testing). ## Returns `void` # resolveBranding > **resolveBranding**(`input?`): [`ResolvedBranding`](/revturbine-sdk/api/interfaces/resolvedbranding/) Resolve branding down the four-rung ladder. Pure and synchronous: fetching the Branding API (rung 3) is the host's job — pass the result as `apiBranding`. ## Parameters ### input? [`BrandingResolutionInput`](/revturbine-sdk/api/interfaces/brandingresolutioninput/) = `{}` The available branding sources; see [BrandingResolutionInput](/revturbine-sdk/api/interfaces/brandingresolutioninput/). ## Returns [`ResolvedBranding`](/revturbine-sdk/api/interfaces/resolvedbranding/) The merged branding and the rung it came from. # resolveContent > **resolveContent**(`content`, `context`): [`ResolvedContent`](/revturbine-sdk/api/type-aliases/resolvedcontent/) Resolve all personalization tokens in a content object. ## Parameters ### content `Record`\<`string`, `unknown`\> ### context [`PersonalizationContext`](/revturbine-sdk/api/interfaces/personalizationcontext/) ## Returns [`ResolvedContent`](/revturbine-sdk/api/type-aliases/resolvedcontent/) # resolveLocalPlaybook > **resolveLocalPlaybook**(`localRuntime?`): [`ConfigArtifact`](/revturbine-sdk/api/type-aliases/configartifact/) \| [`UnvalidatedConfigArtifact`](/revturbine-sdk/api/type-aliases/unvalidatedconfigartifact/) \| `undefined` Resolve the local-mode Playbook from either accepted key. `playbook` is canonical; `exportedConfig` is the legacy alias kept for backwards compatibility. Both are optional at the type level, so this is the one place that decides precedence — consumers must not read either key directly or they will disagree about which one wins. The returned artifact may be the raw [UnvalidatedConfigArtifact](/revturbine-sdk/api/type-aliases/unvalidatedconfigartifact/) the caller supplied (e.g. an imported JSON module); the runtime config pipeline validates it via `configArtifactForRuntime` before evaluation. ## Parameters ### localRuntime? `Pick`\<`RevTurbineLocalRuntimeOptions`, `"playbook"` \| `"exportedConfig"`\> \| `null` ## Returns [`ConfigArtifact`](/revturbine-sdk/api/type-aliases/configartifact/) \| [`UnvalidatedConfigArtifact`](/revturbine-sdk/api/type-aliases/unvalidatedconfigartifact/) \| `undefined` # resolvePersistentStorage > **resolvePersistentStorage**(`storage?`): `RevTurbineStorage` Resolve a persistent storage provider. Priority: 1. Explicitly provided `storage` (customer override) 2. `localStorage` when running in a browser 3. In-memory fallback ## Parameters ### storage? `RevTurbineStorage` ## Returns `RevTurbineStorage` # resolveSessionStorage > **resolveSessionStorage**(`storage?`): `RevTurbineStorage` Resolve a session-scoped storage provider. Priority: 1. Explicitly provided `storage` (customer override) 2. `sessionStorage` when running in a browser 3. In-memory fallback ## Parameters ### storage? `RevTurbineStorage` ## Returns `RevTurbineStorage` # resolveTokens > **resolveTokens**(`template`, `context`): `string` Resolve personalization tokens in a string value. Tokens use the `{{token_name}}` format. ## Parameters ### template `string` ### context [`PersonalizationContext`](/revturbine-sdk/api/interfaces/personalizationcontext/) ## Returns `string` # RevTurbineProvider > **RevTurbineProvider**\<`TUser`, `TOptions`\>(`__namedParameters`): `any` React context provider for the RevTurbine SDK. Wraps your application to provide SDK access to all child components. Handles initialization, user identification, and optional placement bootstrap. ## Type Parameters ### TUser `TUser` *extends* [`RevTurbineUserContext`](/revturbine-sdk/api/interfaces/revturbineusercontext/) = [`RevTurbineUserContext`](/revturbine-sdk/api/interfaces/revturbineusercontext/) ### TOptions `TOptions` *extends* [`RevTurbineInitInputOptions`](/revturbine-sdk/api/type-aliases/revturbineinitinputoptions/) = [`RevTurbineInitInputOptions`](/revturbine-sdk/api/type-aliases/revturbineinitinputoptions/) ## Parameters ### \_\_namedParameters `RevTurbineProviderProps`\<`TUser`, `TOptions`\> ## Returns `any` ## Example ```tsx ``` # RevTurbineRoute > **RevTurbineRoute**(`__namedParameters`): `ReactElement` Declares the app route for the slots beneath it (BL-0207 / D-30). Every `usePlacement()` / `` in the subtree stamps this route onto its slot lifecycle events (`slot_evaluated`, `slot_filled`, …), and ingestion-driven slot discovery persists it on the discovered surface slot. Optional: without it, slots report `window.location.pathname` with identifier-like segments templated to `:id`. Mount it when your router knows the route PATTERN, so `/projects/42` and `/projects/43` are one route. Renderless — adds no DOM node. The innermost boundary wins. ## Parameters ### \_\_namedParameters [`RevTurbineRouteProps`](/revturbine-sdk/api/interfaces/revturbinerouteprops/) ## Returns `ReactElement` ## Example ```tsx // Next.js App Router (app/layout.tsx child, a client component) const route = routePatternFromParams(usePathname(), useParams()); return {children}; ``` # RevTurbineThemeProvider > **RevTurbineThemeProvider**(`__namedParameters`): `any` Provides the active [RevTurbineTheme](/revturbine-sdk/api/interfaces/revturbinetheme/) to all child SDK components. Typically rendered internally by [RevTurbineProvider](/revturbine-sdk/api/functions/revturbineprovider/) — consumers don't need to add this manually. The theme is merged over [DEFAULT\_THEME](/revturbine-sdk/api/variables/default_theme/) here, so [useRevTurbineTheme](/revturbine-sdk/api/functions/userevturbinetheme/) always returns a complete theme. That merge used to be the caller's job while the prop was typed as a complete `RevTurbineTheme`, which made a partial theme a runtime crash rather than a type error: components read `theme.typography.fontSize` and got `undefined` on a theme that only set colors. `RevTurbineThemeInput` has always documented itself as "all values are optional — the SDK deep-merges with defaults"; this is the component honouring that. ## Parameters ### \_\_namedParameters `RevTurbineThemeProviderProps` ## Returns `any` # routePatternFromParams > **routePatternFromParams**(`pathname`, `params`): `string` \| `null` Rebuild the framework route PATTERN from a concrete pathname and the framework's route params — the Next.js App Router recipe, without the SDK importing `next/navigation`: ```tsx 'use client'; import { useParams, usePathname } from 'next/navigation'; import { RevTurbineRoute, routePatternFromParams } from '@revturbine/sdk'; export function RouteBoundary({ children }: { children: React.ReactNode }) { const route = routePatternFromParams(usePathname(), useParams()); return {children}; } ``` `/projects/p_42/settings` with `{ projectId: 'p_42' }` becomes `/projects/[projectId]/settings`; a catch-all `{ slug: ['a', 'b'] }` becomes `[...slug]`. The result is then normalized like any other route, so a segment the params did not name is still templated if it looks like an identifier. ## Parameters ### pathname `string` \| `null` \| `undefined` ### params `Readonly`\<`Record`\<`string`, `string` \| readonly `string`[] \| `undefined`\>\> \| `null` \| `undefined` ## Returns `string` \| `null` # Slot > **Slot**(`__namedParameters`): `any` Universal surface slot component. Fetches a placement decision and automatically renders it using the correct registered slot type. Combines `usePlacement` (decision fetching + interaction tracking) with `PlacementRenderer` (slot type resolution + rendering). ## Parameters ### \_\_namedParameters `SurfaceSlotComponentProps` ## Returns `any` ## Example ```tsx handleCTA(uiPath)} fallback={} /> ``` # SmsPreviewComponent > **SmsPreviewComponent**(`__namedParameters`): `any` Static SMS preview: a chat bubble carrying the message body, plus the CTA row (rendered below the bubble). **Content fields used:** `body`, `message`, `cta_label`, `secondary_cta_label` ## Parameters ### \_\_namedParameters [`PlacementSlotProps`](/revturbine-sdk/api/interfaces/placementslotprops/) ## Returns `any` # TelemetryScope > **TelemetryScope**(`__namedParameters`): `ReactElement` Establishes ambient telemetry context (`area` / `action` / `purpose`) for its descendants (plan 144 TASK-12 / REQ-21). Nested scopes merge **inner-over-outer**, and per-event [useTrack](/revturbine-sdk/api/functions/usetrack/) options override the scope (the outer → inner → invocation precedence). It is renderless — renders its children unchanged, adds no DOM node (REQ-19), and works with or without a RevTurbine provider (AC-13). ## Parameters ### \_\_namedParameters [`TelemetryScopeProps`](/revturbine-sdk/api/interfaces/telemetryscopeprops/) ## Returns `ReactElement` # Track > **Track**(`__namedParameters`): `ReactElement` Fire a telemetry event on click (plan 144 TASK-16 / REQ-22, AC-14). With `asChild`, it **composes onto the single child element** rather than wrapping it: the child's own `onClick` runs first, and the telemetry fires only if the child did **not** `preventDefault`. Because it clones the child (no wrapper element, no added `role`/`tabIndex`), the child's accessible name and disabled state are unchanged (REQ-19). Without `asChild` it renders a host element (default ``). ## Parameters ### \_\_namedParameters [`TrackProps`](/revturbine-sdk/api/interfaces/trackprops/) ## Returns `ReactElement` # TrackOnView > **TrackOnView**(`__namedParameters`): `ReactElement` Emits `event` exactly once, the first time its host element is viewport-qualified (plan 144 TASK-13 / REQ-21). Falls back to firing on render when `IntersectionObserver` is unavailable (AC-10 degradation). The one-shot guard is a ref that survives React Strict Mode's development double-mount, so the event fires once, not twice (AC-12). Renders its children in the host element and never throws without a provider (AC-13). ## Parameters ### \_\_namedParameters [`TrackOnViewProps`](/revturbine-sdk/api/interfaces/trackonviewprops/) ## Returns `ReactElement` # unregisterCtaResolver > **unregisterCtaResolver**(`type`): `boolean` Remove a resolver from the default global registry. Returns true if a resolver was registered and removed. ## Parameters ### type `string` ## Returns `boolean` # useAddons > **useAddons**(): `UseAddonsResult` React access to the active user's public, segment-eligible add-on catalog. ## Returns `UseAddonsResult` # useCan > **useCan**(`handle`, `options?`): [`UseCanResult`](/revturbine-sdk/api/interfaces/usecanresult/) Reactive counterpart to the imperative `rt.can(handle)` — the entitlement check as a `can` question. Takes the entitlement handle positionally (matching `rt.can('handle')`), plus the same options [useEntitlement](/revturbine-sdk/api/functions/useentitlement/) accepts, and returns a curated `{ can, limited, isLoading, result }`. Deny-until-ready: `can` is `false` while the check is unresolved, flipping within one microtask in local mode once the Playbook is in memory. Use `isLoading` to distinguish "still deciding" from a settled deny. For the full reactive surface (`allowed` / `denied` / `error` / `gatedPlacement` / `recheck`), use [useEntitlement](/revturbine-sdk/api/functions/useentitlement/) directly. ## Parameters ### handle `string` ### options? `Omit`\<`UseEntitlementOptions`, `"handle"`\> ## Returns [`UseCanResult`](/revturbine-sdk/api/interfaces/usecanresult/) ## Example ```tsx function BatchExport() { const { can, limited, isLoading } = useCan('batch_export'); if (!can && isLoading) return null; // still deciding — render nothing yet if (!can) return ; // settled deny return ; } ``` # useEntitlement > **useEntitlement**(`__namedParameters`): `UseEntitlementResult` React hook that wraps `sdk.checkEntitlement()`. Returns a reactive entitlement result that can drive access-gate UI. Until the first check resolves, `allowed` and `denied` are both `false` and `result` is `null` — the three-state model. Consumers decide from the triple (`isLoading` / `allowed` / `denied`), never from `denied` alone; the SDK is fail-closed, so "not yet allowed" is the default. Evaluation is local to the loaded Playbook — there is no per-check network call. ## Parameters ### \_\_namedParameters `UseEntitlementOptions` ## Returns `UseEntitlementResult` ## Example ```tsx function BrandKitSection() { const { allowed, denied, result } = useEntitlement({ handle: 'brand_kit' }); if (denied) return ; return ; } ``` # useGatedAction > **useGatedAction**\<`A`, `R`\>(`action`, `fn`, `context?`, `options?`): [`GatedAction`](/revturbine-sdk/api/interfaces/gatedaction/)\<`A`, `R`\> The React analog of the headless `rt.gate(action, fn)` (plan 144 TASK-14 / REQ-21, AC-11). `run` **delegates to `sdk.gate`**, so it emits the same active gate sequence — `gate_attempted`, then `gate_allowed` or `gate_denied` — rather than forking it. When allowed, the action runs wrapped in tracked-action telemetry (`${action}_started` → `_completed` / `_failed`) and its return value is preserved. Without a provider, `run` is a safe no-op that reports denied (AC-13). ## Type Parameters ### A `A` *extends* `unknown`[] ### R `R` ## Parameters ### action `string` ### fn (...`args`) => `R` \| `Promise`\<`R`\> ### context? `RevTurbineEntitlementContext` ### options? [`TrackOptions`](/revturbine-sdk/api/interfaces/trackoptions/) ## Returns [`GatedAction`](/revturbine-sdk/api/interfaces/gatedaction/)\<`A`, `R`\> # usePlacement > **usePlacement**(`__namedParameters`): [`UsePlacementResult`](/revturbine-sdk/api/type-aliases/useplacementresult/) React hook for loading a placement decision and managing interactions. Handles registration, decision fetching, impression tracking, and exposes interaction methods (dismiss, remind me later, CTA click/complete). ## Parameters ### \_\_namedParameters [`UsePlacementOptions`](/revturbine-sdk/api/type-aliases/useplacementoptions/) ## Returns [`UsePlacementResult`](/revturbine-sdk/api/type-aliases/useplacementresult/) ## Example ```tsx const { visible, content, ctaClick, dismiss } = usePlacement({ placement: { name: 'pricing_banner' }, userId: 'user_123', }); if (!visible) return null; return (

{content?.header}

); ``` # usePlacementPersonalization > **usePlacementPersonalization**(`__namedParameters`): [`PersonalizationContext`](/revturbine-sdk/api/interfaces/personalizationcontext/) Derive placement personalization tokens from the SDK's registered providers. This hook resolves provider state through `sdk.providerRegistry.resolveAll()` so token values honor provider strategies configured during SDK initialization. ## Parameters ### \_\_namedParameters [`UsePlacementPersonalizationOptions`](/revturbine-sdk/api/interfaces/useplacementpersonalizationoptions/) ## Returns [`PersonalizationContext`](/revturbine-sdk/api/interfaces/personalizationcontext/) # usePlans > **usePlans**(): `UsePlansResult` React access to the active user's public, segment-eligible plan catalog. ## Returns `UsePlansResult` # useResolvedColorScheme > **useResolvedColorScheme**(`preference`): `"light"` \| `"dark"` Resolve a [RevTurbineColorScheme](/revturbine-sdk/api/type-aliases/revturbinecolorscheme/) to the concrete scheme to paint. `'system'` tracks `prefers-color-scheme` and keeps tracking it: a user flipping their OS theme updates the resolved value without a remount, which is the difference between following the system and sampling it once at init. ## Parameters ### preference [`RevTurbineColorScheme`](/revturbine-sdk/api/type-aliases/revturbinecolorscheme/) The requested scheme. `'system'` follows the OS. ## Returns `"light"` \| `"dark"` `'light'` or `'dark'`. # useRevTurbineTheme > **useRevTurbineTheme**(): [`RevTurbineTheme`](/revturbine-sdk/api/interfaces/revturbinetheme/) Access the current SDK theme tokens from any component inside a [RevTurbineProvider](/revturbine-sdk/api/functions/revturbineprovider/) tree. ## Returns [`RevTurbineTheme`](/revturbine-sdk/api/interfaces/revturbinetheme/) # useRevTurbineThemeProviderPresent > **useRevTurbineThemeProviderPresent**(): `boolean` Whether a [RevTurbineThemeProvider](/revturbine-sdk/api/functions/revturbinethemeprovider/) is already mounted above the caller. Used by [RevTurbineProvider](/revturbine-sdk/api/functions/revturbineprovider/) to decide whether to mount its own. Exposed so an app can make the same check — e.g. a design-system wrapper that only supplies a theme when nothing upstream has. ## Returns `boolean` `true` when an ancestor mounted a theme provider. # UserProfile > **UserProfile**(`__namedParameters`): `any` Read-only visualization of the resolved SDK user context. Displays user id, plan, matched segments, usage balances, and traits that were used for segment evaluation and placement decisioning. ## Parameters ### \_\_namedParameters [`UserProfileProps`](/revturbine-sdk/api/interfaces/userprofileprops/) ## Returns `any` # useSlotRoute > **useSlotRoute**(): `string` \| `null` The route a slot rendered here would report: the nearest [RevTurbineRoute](/revturbine-sdk/api/functions/revturbineroute/) pattern, else the browser's normalized pathname, else `null` (server render). Read at call time. ## Returns `string` \| `null` # useSurfaceSlot > **useSurfaceSlot**(`options`): `UseSurfaceSlotResult` Hook that combines `usePlacement` decision loading with automatic rendering via `PlacementRenderer`. Returns everything `usePlacement` returns, plus an `element` property containing a pre-rendered React element. Drop `element` into your JSX to render the placement without writing custom rendering logic. ## Parameters ### options `UseSurfaceSlotOptions` ## Returns `UseSurfaceSlotResult` ## Example ```tsx function FeatureGate() { const { element, visible } = useSurfaceSlot({ surfaceSlot: { id: 'ai-export-gate', name: 'AI Export Gate' }, personalization: { user_name: 'Jane' }, onCtaClick: (uiPath) => handleCTA(uiPath), }); return (

Export

{element}
); } ``` # useTelemetryProps > **useTelemetryProps**(`event`, `data?`, `options?`): `object` Telemetry props to spread onto a primitive that **cannot take a wrapper** and so can't use `` (plan 144 TASK-16 / REQ-22). Returns an `onClick` bound to the enclosing [TelemetryScope](/revturbine-sdk/api/functions/telemetryscope/); the caller composes it with any handler of their own. Respects `preventDefault` — if a prior handler prevents default, the event does not fire. ## Parameters ### event `string` ### data? [`SdkEventProperties`](/revturbine-sdk/api/type-aliases/sdkeventproperties/) ### options? [`TrackOptions`](/revturbine-sdk/api/interfaces/trackoptions/) ## Returns `object` ### onClick > **onClick**: `MouseEventHandler` ## Example ```tsx const t = useTelemetryProps('cta_clicked', { plan: 'pro' }); ``` # useTelemetryScope > **useTelemetryScope**(): [`TelemetryScopeValue`](/revturbine-sdk/api/interfaces/telemetryscopevalue/) The current merged telemetry scope (plan 144 TASK-12). Empty when outside any [TelemetryScope](/revturbine-sdk/api/functions/telemetryscope/). ## Returns [`TelemetryScopeValue`](/revturbine-sdk/api/interfaces/telemetryscopevalue/) # useTrack > **useTrack**(): [`TrackFn`](/revturbine-sdk/api/type-aliases/trackfn/) Returns a `track(name, data?, options?)` function bound to the enclosing [TelemetryScope](/revturbine-sdk/api/functions/telemetryscope/) (plan 144 TASK-12 / REQ-21). - Scope context (`area` / `action` / `purpose`) merges **outer → inner → invocation** — the per-event option wins. - `once` and `dedupeKey` suppress repeats for this hook instance's lifetime. - Reserved property names are dropped so a caller can't overwrite canonical fields. - Without a RevTurbine provider (absent or failed init) the function is a safe no-op and never throws (AC-13). ## Returns [`TrackFn`](/revturbine-sdk/api/type-aliases/trackfn/) # useTrackedAction > **useTrackedAction**\<`A`, `R`\>(`name`, `fn`, `options?`): [`TrackedAction`](/revturbine-sdk/api/interfaces/trackedaction/)\<`A`, `R`\> Wrap an async action so its lifecycle is telemetered (plan 144 TASK-14 / REQ-21). `run` emits `${name}_started`, then `${name}_completed` on success or `${name}_failed` (with a non-sensitive `error_category`) on throw — and **preserves the return type and re-throws**, so it is a drop-in wrapper. Events inherit the enclosing [TelemetryScope](/revturbine-sdk/api/functions/telemetryscope/); a missing provider makes the telemetry a no-op (AC-13) but never swallows the action. ## Type Parameters ### A `A` *extends* `unknown`[] ### R `R` ## Parameters ### name `string` ### fn (...`args`) => `R` \| `Promise`\<`R`\> ### options? [`TrackOptions`](/revturbine-sdk/api/interfaces/trackoptions/) ## Returns [`TrackedAction`](/revturbine-sdk/api/interfaces/trackedaction/)\<`A`, `R`\> # AnalyticsProviderOptions Configuration for [createAnalyticsProvider](/revturbine-sdk/api/functions/createanalyticsprovider/). ## Properties ### filter? > `optional` **filter?**: `string`[] When provided, only events whose `type` matches one of these strings are forwarded. All others are silently dropped. #### Example ```ts ['placement_interaction', 'placement_dismissed'] ``` *** ### handler > **handler**: [`AnalyticsEventHandler`](/revturbine-sdk/api/type-aliases/analyticseventhandler/) Callback that pushes one event to the analytics platform. Called once per SDK event after optional transformation. *** ### name? > `optional` **name?**: `string` Human-readable consumer name shown in diagnostics. #### Default ```ts 'analytics' ``` *** ### transform? > `optional` **transform?**: [`AnalyticsEventTransformer`](/revturbine-sdk/api/type-aliases/analyticseventtransformer/) Optional transform applied before the handler. Use this to rename events, enrich properties, or drop events you don't care about. Return `null` to suppress the event. # AssignSeatTypeOptions Options for [RevTurbineServer.assignSeatType](/revturbine-sdk/api/classes/revturbineserver/#assignseattype): built-in dimension values to write in the SAME server-key upsert as the seat (BL-0382). See [SetBuiltinDimensionsOptions](/revturbine-sdk/api/interfaces/setbuiltindimensionsoptions/) for `override`. ## Properties ### builtinDimensions? > `optional` **builtinDimensions?**: `object` Built-in dimension values to set (or clear with `null`) with the seat. *** ### override? > `optional` **override?**: `boolean` Pin the `builtinDimensions` written in this call (D-47). Default `false`. # BasicBucketerExperiment One experiment's arms, and optionally how much of the population sees it. ## Properties ### exposure? > `optional` **exposure?**: `number` Fraction of subjects enrolled at all, 0..1. Default 1 (everyone). A subject outside it is reported as NOT ENROLLED — the experiment is simply absent from the assignment map, which is distinct from being assigned to a control arm. That distinction is the whole point of a holdout: control is a measured arm, unenrolled is not in the experiment. *** ### variants > **variants**: readonly `string`[] \| `Readonly`\<`Record`\<`string`, `number`\>\> Either an equal split across the listed variant handles, or explicit relative weights. Weights need not sum to anything in particular — they are normalized — so `{ control: 1, variant_b: 3 }` is a 25/75 split. A variant weighted 0 (or negative) is excluded rather than made unreachable-but-present, so removing an arm and zeroing it behave the same. # BrandingResolutionInput Inputs to [resolveBranding](/revturbine-sdk/api/functions/resolvebranding/). Every field is optional; with none supplied the ladder resolves to [DEFAULT\_BRANDING](/revturbine-sdk/api/variables/default_branding/). ## Properties ### apiBranding? > `optional` **apiBranding?**: `object` Rung 3 — branding fetched from the Branding API / workspace settings. *** ### colorScheme? > `optional` **colorScheme?**: `"light"` \| `"dark"` Resolved display palette used when no branding theme was supplied. *** ### explicit? > `optional` **explicit?**: `object` Rung 1 — branding passed explicitly to the SDK at init. *** ### legacyConfigTheme? > `optional` **legacyConfigTheme?**: `Record`\<`string`, `unknown`\> Rung 2 — the legacy `theme` embedded in an older config artifact. Deprecated: supplying it resolves correctly but emits a development-only warning. *** ### warnOnLegacy? > `optional` **warnOnLegacy?**: `boolean` When `true` (the default), using the legacy rung logs a one-time development deprecation warning. Set `false` to silence it (e.g. in tests). # CanonicalExperimentAllocation The allocation fields owned by one canonical Experiment version. Derived from the schema type rather than redeclared, so schema evolution is visible here at compile time without making the adapter require unrelated persistence fields. ## Properties ### assignment\_unit? > `optional` **assignment\_unit?**: `"user"` \| `"account"` \| `"billing_unit"` \| `"organization"` Unit represented by the stable subject identifier; defaults to `user`. *** ### sequence? > `optional` **sequence?**: `number` Canonical experiment version — the `Experiment.sequence` this allocation was taken from (plan 224, war-games spec §9.1). When present, the native provider derives an [ExperimentAssignmentFactDeclaration](/revturbine-sdk/api/interfaces/experimentassignmentfactdeclaration/) so assignments emit the `experiment_assigned` assignment fact with the real (handle, version) SRM join key. When absent, assignment still works but no fact is emitted for this experiment — a fact with a fabricated version is worse than a missing one. # ClientSessionResult Result of minting a client session — a short-lived, opaque browser token. Hand `client_token` to the frontend; it authenticates `GET /api/sdk/client-context`. It carries no user id in the request (the token determines the subject), and expires at `expires_at`. ## Properties ### client\_token > **client\_token**: `string` The opaque `rt_client_` token to return to the browser. *** ### expires\_at > **expires\_at**: `string` ISO-8601 timestamp at which the token expires. # CompositeExperimentProviderOptions Controls aggregation of customer-owned experiment adapters. ## Properties ### timeoutMs? > `optional` **timeoutMs?**: `number` Maximum time for one adapter resolution. Set to `0` to disable. # ConfigTargetDefaults The target (tenant + environment) the SDK was initialized against. These are the **authority** on which tenant a runtime decides for. An artifact's own `tenant_id` / `environment_id` are a *guard*: they exist so the CLI can refuse to upload a config to the wrong tenant while setting up demo environments, and they are not a runtime input (plan 233 REQ-2, Kent 2026-09-10). A disagreement between the two warns and proceeds on these values — it never fails init. See [normalizeConfigArtifactOrThrow](/revturbine-sdk/api/functions/normalizeconfigartifactorthrow/). ## Properties ### environmentId > **environmentId**: `string` Environment the SDK was initialized with. Wins over the artifact's `environment_id`. *** ### tenantId? > `optional` **tenantId?**: `string` Tenant the SDK was initialized with. Wins over the artifact's `tenant_id`. Optional since `0.11.14` (BL-0335): a key-only browser init passes none, and the artifact's own `tenant_id` — stamped by the control plane for the public key's tenant — is used. # ControlPlaneEmitInput The `(eventName, properties)` pair produced by [buildControlPlaneEvent](/revturbine-sdk/api/functions/buildcontrolplaneevent/). `eventName` is the control-plane `event_type`; `properties` is the payload with the source classification stamped under [CONTROL\_PLANE\_SOURCE\_KEY](/revturbine-sdk/api/variables/control_plane_source_key/). # CreateClientSessionInput Input for minting a short-lived, per-user client-session token. The customer backend attests the end user, then mints a browser-safe token scoped to exactly that subject. The tenant is derived server-side from the server key ([RevTurbineServerOptions.apiKey](/revturbine-sdk/api/interfaces/revturbineserveroptions/#apikey)) — never from this input. ## Properties ### capabilities? > `optional` **capabilities?**: `string`[] Client-facing capabilities the token is scoped to. Defaults to `['context:read']` (fetch the user's client-safe context). *** ### subject > **subject**: `string` The end-user subject the token is minted for (opaque to RevTurbine). *** ### surface? > `optional` **surface?**: `string` Optional surface / session scope hint (e.g. a page or feature area). # CtaResolverContext Context handed to a [CtaResolver](/revturbine-sdk/api/type-aliases/ctaresolver/) when a placement CTA is activated. ## Properties ### kind > **kind**: `"primary"` \| `"secondary"` Which CTA fired — the primary button or a secondary action. *** ### placement > **placement**: `object` The full placement output whose CTA was activated. *** ### remindLater? > `optional` **remindLater?**: (`seconds?`) => `void` Report snooze through the mounted controller and close this occurrence. #### Parameters ##### seconds? `number` #### Returns `void` # EligibleAddon A public add-on variation eligible for the active targeting context. # EligiblePlan A public plan variation eligible for the active targeting context. # EligiblePrice Non-authoritative display price carried by an eligible variation. # EngagementAreaProps Props for [EngagementArea](/revturbine-sdk/api/functions/engagementarea/). ## Properties ### area > **area**: `string` Logical area name — set as the telemetry scope for descendants and stamped on emitted events. *** ### as? > `optional` **as?**: `string` \| `number` \| `symbol` Host element tag. Default `'div'`. *** ### dwellEvent? > `optional` **dwellEvent?**: `string` Event emitted with accrued dwell (`dwell_ms`) when the area unmounts. Default `engagement_dwell`. *** ### interactionEvent? > `optional` **interactionEvent?**: `string` Event emitted when a descendant is clicked. Default `engagement_interaction`. *** ### purpose? > `optional` **purpose?**: [`TrackPurpose`](/revturbine-sdk/api/type-aliases/trackpurpose/) Advisory purpose applied to the scope and emitted events. *** ### threshold? > `optional` **threshold?**: `number` Visible fraction required to count as viewed / dwelling (0–1). Default 0.5. *** ### viewEvent? > `optional` **viewEvent?**: `string` Event emitted once when the area is first qualified-viewed. Default `engagement_view`. # EntitlementGateOptions Options for creating an EntitlementGate. ## Properties ### autoGate? > `optional` **autoGate?**: `boolean` When true, automatically resolve a gated placement for denied entitlements. Default `false`. *** ### context? > `optional` **context?**: `RevTurbineEntitlementContext` Optional context (usage, required tier, etc.). *** ### gatePlacementRequest? > `optional` **gatePlacementRequest?**: `Omit`\<`RevTurbinePlacementRequestConfig`, `"entitlementHandle"`\> Optional placement request fields used when auto-gating needs to fetch a placement and one is not attached to the entitlement response. *** ### handle > **handle**: `string` The entitlement handle to check (e.g. `'brand_kit'`, `'mp4_download'`). # EntitlementGateState Read-only snapshot of an EntitlementGate's state. # ExperimentAssignmentFactDeclaration Binds one experiment handle to the canonical-version metadata its `experiment_assigned` assignment facts must carry (plan 224, spec §9.1). Supply these either through `RevTurbineInitOptions.experimentAssignmentFacts` (works for ANY assignment source, external adapters included) or — for the native provider — via `CanonicalExperimentAllocation.sequence`, which derives a declaration automatically. An assigned experiment with no declaration emits no fact. ## Properties ### assignmentUnit? > `optional` **assignmentUnit?**: [`AssignmentFactUnit`](/revturbine-sdk/api/type-aliases/assignmentfactunit/) Unit the subject identifier represents. Defaults to `'user'`, matching the native provider's default. *** ### experimentHandle > **experimentHandle**: `string` Canonical experiment handle the declaration applies to. *** ### experimentVersion > **experimentVersion**: `number` Canonical experiment version — the `Experiment.sequence` of the version whose allocation is live. Integer ≥ 1; the fact's (handle, version) pair is the SRM join key, so this must be the real canonical version, never a guess. *** ### subject? > `optional` **subject?**: `string` \| (() => `string` \| `null` \| `undefined`) Stable subject identifier for the declared unit. A function is re-read on every emission so a later identity change takes effect; a bare string pins one subject. When omitted, the SDK's user-context `id` is used. With no resolvable subject there is no fact — absent beats wrong. # ExperimentAssignmentFactMetadataCarrier Optional carrier surface for assignment-fact metadata on an experiment provider. The native provider implements it when its allocations carry `sequence`; the SDK harvests declarations from every registered experiment provider that exposes it. ## Properties ### assignmentFactDeclarations? > `readonly` `optional` **assignmentFactDeclarations?**: readonly [`ExperimentAssignmentFactDeclaration`](/revturbine-sdk/api/interfaces/experimentassignmentfactdeclaration/)[] Declarations describing the canonical versions this provider allocates. # FeatureGateTriggerPayload Payload for feature-gate trigger events. # GatedAction State + runner returned by [useGatedAction](/revturbine-sdk/api/functions/usegatedaction/). ## Type Parameters ### A `A` *extends* `unknown`[] ### R `R` ## Properties ### denied > **denied**: `boolean` True when the last run was denied by the entitlement. *** ### entitlement > **entitlement**: \{ \} \| `null` The entitlement result from the last run, or `null`. *** ### error > **error**: [`ActionErrorCategory`](/revturbine-sdk/api/type-aliases/actionerrorcategory/) \| `null` The last action failure's normalized category, or `null`. *** ### isRunning > **isRunning**: `boolean` True while the gate check / action is in flight. *** ### run > **run**: (...`args`) => `Promise`\<[`RevTurbineGateResult`](/revturbine-sdk/api/type-aliases/revturbinegateresult/)\<`R`\>\> Run the gated action. Returns the [RevTurbineGateResult](/revturbine-sdk/api/type-aliases/revturbinegateresult/): `ran: true` with the result, or `ran: false` with the entitlement. #### Parameters ##### args ...`A` #### Returns `Promise`\<[`RevTurbineGateResult`](/revturbine-sdk/api/type-aliases/revturbinegateresult/)\<`R`\>\> # InitFailureDiagnosticProps Development-only visual surface for a failed SDK initialization. Plan 233 TASK-2. The provider renders this beside `children` when init threw and the build is not production. It exists because the app renders correctly without RevTurbine **by design** — a failed init changes nothing a developer can see, so the only prior signal was one `console.error` line competing with everything else in the log. That cost a customer 36 hours. Deliberately self-contained: inline styles, no theme tokens, no portal. The theme itself resolves during init, so a diagnostic that depended on it would be least reliable exactly when it is needed. It must never throw — a crashing error reporter is worse than none. ## Properties ### status > **status**: [`RevTurbineInitStatus`](/revturbine-sdk/api/interfaces/revturbineinitstatus/) The failed status. Rendering is the caller's decision; this only draws. # NativeExperimentAssignmentOptions Options for the native assignment provider backed by Experiment versions. ## Properties ### experiments > **experiments**: readonly [`CanonicalExperimentAllocation`](/revturbine-sdk/api/interfaces/canonicalexperimentallocation/)[] Canonical, versioned Experiment allocations to execute. *** ### subject > **subject**: `string` \| (() => `string` \| `null` \| `undefined`) Stable assignment subject, re-read per resolution when supplied as a function. *** ### subjectUnit? > `optional` **subjectUnit?**: [`NativeExperimentAssignmentUnit`](/revturbine-sdk/api/type-aliases/nativeexperimentassignmentunit/) Unit represented by `subject`; defaults to `user` for backward compatibility. # PaymentTriggerPayload Payload for payment-related trigger events. # PersonalizationContext Personalization token context for resolving `{{token}}` placeholders in content. # PlacementContentFields Content fields common to most surface templates. Individual placement slot types extend or narrow these. # PlacementControllerOptions Options for creating a [PlacementController](/revturbine-sdk/api/classes/placementcontroller/). ## Properties ### autoTrackImpression? > `optional` **autoTrackImpression?**: `boolean` Whether to track an impression automatically when the decision is visible. Default `true`. *** ### contextMode? > `optional` **contextMode?**: `RevTurbineContextMode` Context resolution mode. Default `'auto'`. *** ### overrides? > `optional` **overrides?**: `RevTurbinePlacementDecisionOverrides` Override segment, plan, or usage for testing. *** ### placement? > `optional` **placement?**: [`RevTurbinePlacementConfig`](/revturbine-sdk/api/interfaces/revturbineplacementconfig/) Placement configuration (name, scope key, metadata). *** ### placementExposure? > `optional` **placementExposure?**: `PlacementExposureMode` When the presentation-writing `impression` fires (plan 144 TASK-11 / REQ-17). - `legacy_resolution` (default) — at decision resolution, exactly as today. - `render` — when the placement's visual root renders. - `viewport` — when the root scrolls into the viewport; if `IntersectionObserver` is unavailable it falls back to resolution (Q-8 ruling), tagged `render_fallback`. Only `viewport` (with `IntersectionObserver` present) moves the `placement_presentations` denominator — a deliberate metric-definition change (see the plan's Q-8 / REQ-17b). The default keeps the denominator and its history unchanged. *** ### route? > `optional` **route?**: `string` \| (() => `string` \| `null` \| `undefined`) The app route this slot renders on, stamped onto every slot lifecycle event as `route` (BL-0207 / D-30) so ingestion-driven discovery can persist it on the discovered surface slot. A string, or a getter read at emission time (so a slot that outlives a client-side navigation reports the route it was evaluated on). Either is normalized by [normalizeSlotRoute](/revturbine-sdk/api/functions/normalizeslotroute/): origin, query string and fragment are dropped and identifier-like segments templated to `:id`. The React `usePlacement()` hook supplies this automatically — the nearest `` pattern, else `window.location.pathname`. Omitted, a headless controller emits `route: null`. *** ### surfaceSlot? > `optional` **surfaceSlot?**: [`RevTurbineSurfaceSlotConfig`](/revturbine-sdk/api/interfaces/revturbinesurfaceslotconfig/) Canonical surface slot configuration (preferred over `placement`). *** ### traits? > `optional` **traits?**: `Record`\<`string`, `string` \| `number` \| `boolean`\> Custom traits to include in the decision request. *** ### ttlMs? > `optional` **ttlMs?**: `number` Decision cache TTL in milliseconds. *** ### userId? > `optional` **userId?**: `string` Target user ID. Falls back to the SDK's current user context. # PlacementControllerState Read-only snapshot of a [PlacementController](/revturbine-sdk/api/classes/placementcontroller/)'s state. ## Properties ### exposureBasis > `readonly` **exposureBasis**: `ExposureBasis` \| `null` How the placement's visual root came to be considered exposed, or `null` before [PlacementController.markVisible](/revturbine-sdk/api/classes/placementcontroller/#markvisible) is called (plan 144 TASK-9). `'render_fallback'` when `IntersectionObserver` was unavailable (AC-10). # PlacementCustomCode Custom code bundle uploaded by a customer for custom placement rendering. ## Properties ### code > **code**: `string` The React component source code (JSX/TSX). *** ### css? > `optional` **css?**: `string` Optional CSS/styling to apply. *** ### id > **id**: `string` Unique identifier for this code bundle. *** ### slotTypeId > **slotTypeId**: `string` The placement slot type this code renders. *** ### updatedAt > **updatedAt**: `string` When this code was last updated. *** ### version > **version**: `string` Version identifier for cache busting. # PlacementDecisionInspectorProps Props for [PlacementDecisionInspector](/revturbine-sdk/api/functions/placementdecisioninspector/). ## Properties ### autoLoad? > `optional` **autoLoad?**: `boolean` Auto-load explanation on mount and dependency changes. Defaults to `true`. *** ### className? > `optional` **className?**: `string` Optional class name for the root container. *** ### contextMode? > `optional` **contextMode?**: `RevTurbineContextMode` Context resolution mode. Defaults to `'auto'`. *** ### overrides? > `optional` **overrides?**: `RevTurbinePlacementDecisionOverrides` Optional segment/plan/usage overrides for debugging. *** ### ~~placement?~~ > `optional` **placement?**: [`RevTurbinePlacementConfig`](/revturbine-sdk/api/interfaces/revturbineplacementconfig/) Placement registration config used to resolve a placement ID. :::caution[Deprecated] Use `surfaceSlot` instead. ::: *** ### showRawJson? > `optional` **showRawJson?**: `boolean` Whether to render full JSON dumps for deep debugging. Defaults to `false`. *** ### style? > `optional` **style?**: `any` Optional inline styles for the root container. *** ### surfaceSlot? > `optional` **surfaceSlot?**: `Pick`\<[`RevTurbineSurfaceSlotConfig`](/revturbine-sdk/api/interfaces/revturbinesurfaceslotconfig/), `"name"` \| `"id"` \| `"surfaceTemplateIds"`\> Surface slot config used to resolve a placement ID. Preferred over `placement`. *** ### traits? > `optional` **traits?**: `Record`\<`string`, `string` \| `number` \| `boolean`\> Optional traits merged into the decision request. *** ### ttlMs? > `optional` **ttlMs?**: `number` Optional decision cache TTL for the underlying decision call. *** ### userId? > `optional` **userId?**: `string` Target user ID. Falls back to the SDK's current user context. # PlacementPreviewConfig Configuration for the placement preview sandbox. ## Properties ### device? > `optional` **device?**: `"desktop"` \| `"mobile"` \| `"tablet"` Device form factor for preview. *** ### personalization? > `optional` **personalization?**: [`PersonalizationContext`](/revturbine-sdk/api/interfaces/personalizationcontext/) Personalization context for token resolution. *** ### placement > **placement**: `object` The placement output to preview. *** ### sandboxed? > `optional` **sandboxed?**: `boolean` Whether to render in a sandboxed iframe. *** ### viewportHeight? > `optional` **viewportHeight?**: `number` Viewport height for preview. *** ### viewportWidth? > `optional` **viewportWidth?**: `number` Viewport width for preview. # PlacementSlotProps Props passed to every placement slot renderer. The generic parameter `C` allows custom slot types to narrow the `content` field to a specific shape, eliminating `unknown` casts: ```ts interface MyContent extends ResolvedContent { modal_type: string; benefits: Array<{ text: string }>; } function MySlot(props: PlacementSlotProps) { props.content.modal_type; // string — no cast needed } ``` ## Type Parameters ### C `C` *extends* [`ResolvedContent`](/revturbine-sdk/api/type-aliases/resolvedcontent/) = [`ResolvedContent`](/revturbine-sdk/api/type-aliases/resolvedcontent/) ## Properties ### className? > `optional` **className?**: `string` Custom CSS class name for styling overrides. *** ### content > **content**: `C` Resolved content fields with personalization tokens expanded. *** ### exposureRef? > `optional` **exposureRef?**: (`element`) => `void` Optional exposure ref (plan 144 TASK-9 / REQ-18). Attach it to the slot's true visual root so RevTurbine can observe when the placement actually enters the viewport for viewport-qualified exposure; when `IntersectionObserver` is unavailable it falls back to render (`exposure_basis: 'render_fallback'`). **Additive and optional** — omitting it changes nothing, and it never requires a wrapper element (REQ-18/REQ-19). #### Parameters ##### element `Element` \| `null` #### Returns `void` *** ### onCtaClick > **onCtaClick**: () => `void` Callback when user clicks the primary CTA. #### Returns `void` *** ### onDismiss > **onDismiss**: () => `void` Callback when user dismisses the placement. #### Returns `void` *** ### onRemindLater? > `optional` **onRemindLater?**: () => `void` Optional callback when the user chooses "Remind me later" (defer). When provided, dismissible slots render a defer control beside dismiss; the placement re-queues and reappears after the remind-later window (`remind_later_minutes`, default 60) rather than the longer dismiss cooldown (plan 167 REQ-7). **Additive and optional** — omit it and slots render exactly as before, with no defer control. #### Returns `void` *** ### onSecondaryCtaClick? > `optional` **onSecondaryCtaClick?**: () => `void` Callback when user clicks a secondary CTA. #### Returns `void` *** ### placement > **placement**: `object` The full placement output from the decision engine. *** ### promotion? > `optional` **promotion?**: `object` Optional promotion data. *** ### style? > `optional` **style?**: `any` Custom inline styles. *** ### uiPath > **uiPath**: [`PlacementUiPath`](/revturbine-sdk/api/interfaces/placementuipath/) Parsed UI path for CTA handling. *** ### visible > **visible**: `boolean` Whether the placement is currently visible. # PlacementSlotType A placement slot type definition. Each slot type provides: - A unique `id` matching a surface type or custom identifier - A human-readable `label` - A description of the slot behavior - A React component that renders the placement - Optional `defaultProps` for sensible defaults - An `accepts` predicate controlling which outputs this type handles ## Type Parameters ### P `P` *extends* [`PlacementSlotProps`](/revturbine-sdk/api/interfaces/placementslotprops/) = [`PlacementSlotProps`](/revturbine-sdk/api/interfaces/placementslotprops/) ## Properties ### accepts? > `optional` **accepts?**: (`output`) => `boolean` Predicate to determine if this slot type can handle a given output. Defaults to matching on `componentType`. Custom implementations can match on template name, content shape, etc. #### Parameters ##### output #### Returns `boolean` *** ### component > **component**: `ComponentType`\<`P`\> The React component that renders this placement type. *** ### componentType > **componentType**: `"inline"` \| `"custom"` \| `"push"` \| `"banner"` \| `"modal"` \| `"tooltip"` \| `"sidebar"` \| `"toast"` \| `"fullscreen"` \| `"email"` \| `"sms"` \| `"in_page"` \| `"button"` \| `"full_page"` \| `"agent"` \| `"cli"` The canonical component type this renderer handles. *** ### defaultProps? > `optional` **defaultProps?**: `Partial`\<`P`\> Default props merged with resolved props before rendering. *** ### description > **description**: `string` Description of this placement type. *** ### id > **id**: `string` Unique identifier for this slot type (e.g. 'banner', 'modal', 'custom:my-widget'). *** ### label > **label**: `string` Human-readable label for the studio UI. *** ### priority > **priority**: `number` Resolution priority. Higher values are evaluated first when multiple slot types match the same surface type. Use this to let narrow/specific predicates take precedence over broad ones without relying on registration order. Default `0`. *** ### renderedFields > **renderedFields**: readonly `string`[] Canonical content fields this component renders. Validation and Studio previews use this contract to identify authored fields the component ignores. # PlacementUiPath UI path action that the CTA triggers. ## Properties ### params? > `optional` **params?**: `Record`\<`string`, `unknown`\> Non-whitelisted config keys carried by a tenant-defined custom CTA. The engine spreads a custom CTA's authored `config` into the `cta_path` record; [parseUiPath](/revturbine-sdk/api/functions/parseuipath/) lifts the known fields (`url`, `plan_handle`, …) onto this object and collects every remaining key here so a custom [CtaResolver](/revturbine-sdk/api/type-aliases/ctaresolver/) can read tenant-specific parameters. Omitted when the action carries no extra params. # PostHogAnalyticsProviderOptions Configuration for [createPostHogAnalyticsProvider](/revturbine-sdk/api/functions/createposthoganalyticsprovider/). ## Extended by - [`PostHogIntegrationOptions`](/revturbine-sdk/api/interfaces/posthogintegrationoptions/) ## Properties ### filter? > `optional` **filter?**: `string`[] When provided, only events whose `type` matches one of these strings are forwarded to PostHog. Use e.g. the control-plane event types to forward only semantic control-plane events. All others are silently dropped. *** ### name? > `optional` **name?**: `string` Human-readable consumer name shown in diagnostics. #### Default ```ts 'posthog' ``` *** ### posthog > **posthog**: [`PostHogLike`](/revturbine-sdk/api/interfaces/posthoglike/) The PostHog client instance (`posthog-js` or `posthog-node`). *** ### transform? > `optional` **transform?**: [`AnalyticsEventTransformer`](/revturbine-sdk/api/type-aliases/analyticseventtransformer/) Optional transform applied before capture (rename / enrich / drop). # PostHogIntegrationOptions Configuration for [createPostHogIntegration](/revturbine-sdk/api/functions/createposthogintegration/). ## Extends - [`PostHogAnalyticsProviderOptions`](/revturbine-sdk/api/interfaces/posthoganalyticsprovideroptions/) ## Properties ### filter? > `optional` **filter?**: `string`[] When provided, only events whose `type` matches one of these strings are forwarded to PostHog. Use e.g. the control-plane event types to forward only semantic control-plane events. All others are silently dropped. #### Inherited from [`PostHogAnalyticsProviderOptions`](/revturbine-sdk/api/interfaces/posthoganalyticsprovideroptions/).[`filter`](/revturbine-sdk/api/interfaces/posthoganalyticsprovideroptions/#filter) *** ### mirrorNavigation? > `optional` **mirrorNavigation?**: `boolean` Mirror `page_view` events to a native PostHog `$pageview`. Default `false`. *** ### name? > `optional` **name?**: `string` Human-readable consumer name shown in diagnostics. #### Default ```ts 'posthog' ``` #### Inherited from [`PostHogAnalyticsProviderOptions`](/revturbine-sdk/api/interfaces/posthoganalyticsprovideroptions/).[`name`](/revturbine-sdk/api/interfaces/posthoganalyticsprovideroptions/#name) *** ### posthog > **posthog**: [`PostHogLike`](/revturbine-sdk/api/interfaces/posthoglike/) The PostHog client instance (`posthog-js` or `posthog-node`). #### Inherited from [`PostHogAnalyticsProviderOptions`](/revturbine-sdk/api/interfaces/posthoganalyticsprovideroptions/).[`posthog`](/revturbine-sdk/api/interfaces/posthoganalyticsprovideroptions/#posthog) *** ### syncAccountGroup? > `optional` **syncAccountGroup?**: `boolean` Mirror the `account_id` trait to a PostHog group (`group('account', id)`) when it changes. Default `false`. *** ### syncIdentity? > `optional` **syncIdentity?**: `boolean` Mirror RevTurbine identify / logout to `posthog.identify` / `posthog.reset`. The distinct id is RevTurbine's already-redacted (hashed) user id — never a raw email. Default `false`. *** ### transform? > `optional` **transform?**: [`AnalyticsEventTransformer`](/revturbine-sdk/api/type-aliases/analyticseventtransformer/) Optional transform applied before capture (rename / enrich / drop). #### Inherited from [`PostHogAnalyticsProviderOptions`](/revturbine-sdk/api/interfaces/posthoganalyticsprovideroptions/).[`transform`](/revturbine-sdk/api/interfaces/posthoganalyticsprovideroptions/#transform) # PostHogLike Minimal shape of a PostHog client — satisfied by both `posthog-js` (browser) and `posthog-node` (server). `capture` is always used; the identity lifecycle methods are optional and only called by [createPostHogIntegration](/revturbine-sdk/api/functions/createposthogintegration/) when the matching sync flag is on. This is a structural interface — no PostHog package is imported or bundled (the client is injected). ## Methods ### group()? > `optional` **group**(`groupType`, `groupKey`, `properties?`): `void` Optional — mirrors an account group when `syncAccountGroup` is on. #### Parameters ##### groupType `string` ##### groupKey `string` ##### properties? `Record`\<`string`, `unknown`\> #### Returns `void` *** ### identify()? > `optional` **identify**(`distinctId`, `properties?`): `void` Optional — mirrors a RevTurbine identify when `syncIdentity` is on. #### Parameters ##### distinctId `string` ##### properties? `Record`\<`string`, `unknown`\> #### Returns `void` *** ### reset()? > `optional` **reset**(): `void` Optional — called when the identity is cleared (logout) under `syncIdentity`. #### Returns `void` # ResolvedBranding The outcome of [resolveBranding](/revturbine-sdk/api/functions/resolvebranding/): the fully-merged branding plus the rung it was resolved from. ## Properties ### branding > `readonly` **branding**: `object` Fully-populated branding, each rung merged over [DEFAULT\_BRANDING](/revturbine-sdk/api/variables/default_branding/). *** ### source > `readonly` **source**: [`BrandingSource`](/revturbine-sdk/api/type-aliases/brandingsource/) Which ladder rung supplied the (top-priority) branding. # RevTurbineApiPaths This file was auto-generated by openapi-typescript. Do not make direct changes to the file. # RevTurbineAuthoredSlotRef A Playbook placement that targets a surface slot. ## Properties ### category? > `optional` **category?**: `string` The placement's category, when the Playbook declares one. *** ### placementId > **placementId**: `string` The authored placement's id. *** ### slotId > **slotId**: `string` The slot id the placement targets (`trigger.slot_id`). # RevTurbineChainedPlacementRequestOptions Optional context for chained placement requests. ## Properties ### ~~surfaceType?~~ > `optional` **surfaceType?**: `"inline"` \| `"custom"` \| `"push"` \| `"banner"` \| `"modal"` \| `"tooltip"` \| `"sidebar"` \| `"toast"` \| `"fullscreen"` \| `"email"` \| `"sms"` \| `"in_page"` \| `"button"` \| `"full_page"` \| `"agent"` \| `"cli"` :::caution[Deprecated] Use `componentType`. ::: # RevTurbineCheckoutMetadata What [RevTurbineCustomerSdk.checkoutMetadata](/revturbine-sdk/api/classes/revturbinecustomersdk/#checkoutmetadata) returns: the bag to put on the Checkout Session and its subscription, plus Stripe's own caller-side session key. ## Properties ### client\_reference\_id? > `optional` **client\_reference\_id?**: `string` The user id, for the session's `client_reference_id` — Stripe's native caller-side key, which RevTurbine's identity bridge also reads. The same value as `metadata.revturbine_user_id`; absent for an unidentified user. *** ### metadata > **metadata**: [`RevTurbineCheckoutMetadataBag`](/revturbine-sdk/api/interfaces/revturbinecheckoutmetadatabag/) Pass as BOTH `metadata` and `subscription_data.metadata`. # RevTurbineCheckoutMetadataBag The Stripe Checkout `revturbine_*` metadata bag for a rendered output (plan 282 REQ-5; research §6.2). String-valued and Stripe-shaped: spread it into a Checkout Session's `metadata` and `subscription_data.metadata` as-is. A key the SDK does not hold is ABSENT, never empty or a placeholder — Stripe reads an empty metadata value as an unset, and an invented id would be worse than a gap on the attribution side. ## Properties ### revturbine\_account\_id? > `optional` **revturbine\_account\_id?**: `string` The identified account, redacted like the wire lanes. Absent when none was identified — never the user-derived fallback the interaction lane labels. *** ### revturbine\_decision\_id? > `optional` **revturbine\_decision\_id?**: `string` `PlacementOutput.decision_id`. *** ### revturbine\_output\_id > **revturbine\_output\_id**: `string` `PlacementOutput.output_id` — the rendered payload / treatment. *** ### revturbine\_placement\_id > **revturbine\_placement\_id**: `string` The placement whose CTA led to checkout — what confirms credit to it. *** ### revturbine\_plan\_handle? > `optional` **revturbine\_plan\_handle?**: `string` The plan the CTA converts onto (`cta_path.plan_handle`), when it names one. *** ### revturbine\_rule\_id? > `optional` **revturbine\_rule\_id?**: `string` `PlacementOutput.rule_id` — the rule handle the interaction lanes stamp as `rule_handle`. *** ### revturbine\_user\_id? > `optional` **revturbine\_user\_id?**: `string` The id `identify()` set, as this SDK's own telemetry carries it: an email-shaped id is hashed the same way on every lane, so the purchase joins the presentation and click rows. Absent for an unidentified user. # RevTurbineConfigProvider Provider abstraction for Playbook access inside the SDK. Local mode typically uses a static provider backed by `localRuntime.playbook` (or its deprecated `exportedConfig` alias). Other modes can provide custom or REST-backed resolvers via `refresh()`. Implement **`getPlaybook()`** — `getExportedConfig()` is the deprecated spelling of the same accessor, kept so an already-shipped implementation keeps compiling. Both are optional *on this interface* so that neither spelling breaks an existing `implements` clause; the RevTurbineInitOptions.configProvider option intersects this with a union that still requires exactly one of them, so a provider supplying neither remains a compile error at the call site it is passed to. ## Methods ### ~~getExportedConfig()?~~ > `optional` **getExportedConfig**(): [`ConfigArtifact`](/revturbine-sdk/api/type-aliases/configartifact/) \| `undefined` :::caution[Deprecated] Renamed to [RevTurbineConfigProvider.getPlaybook](/revturbine-sdk/api/interfaces/revturbineconfigprovider/#getplaybook) — `Playbook` is the canonical name for the artifact (BL-0156). Still fully supported: implement either one. When both are present `getPlaybook()` wins. Removed in `0.13.0`. ::: #### Returns [`ConfigArtifact`](/revturbine-sdk/api/type-aliases/configartifact/) \| `undefined` *** ### getPlaybook()? > `optional` **getPlaybook**(): [`ConfigArtifact`](/revturbine-sdk/api/type-aliases/configartifact/) \| `undefined` Return the latest available Playbook snapshot. #### Returns [`ConfigArtifact`](/revturbine-sdk/api/type-aliases/configartifact/) \| `undefined` *** ### refresh()? > `optional` **refresh**(): `Promise`\<[`ConfigArtifact`](/revturbine-sdk/api/type-aliases/configartifact/) \| `undefined`\> Optionally refresh the snapshot (for example, from a REST API). #### Returns `Promise`\<[`ConfigArtifact`](/revturbine-sdk/api/type-aliases/configartifact/) \| `undefined`\> # RevTurbineContextPolicy Controls automatic context inference behavior. All flags default to `true` when omitted. ## Properties ### inferPage? > `optional` **inferPage?**: `boolean` Infer page context (URL, title, referrer) from `window.location`. *** ### inferUser? > `optional` **inferUser?**: `boolean` Infer anonymous user context from browser APIs. *** ### routerAutoTrack? > `optional` **routerAutoTrack?**: `boolean` Automatically track SPA route changes via History API patching. # RevTurbineEndpointOverrides Per-operation endpoint path/URL overrides for `custom_endpoints` mode. Each key maps to a fully-qualified URL or an `endpoint`-relative path. ## Properties ### clientContext? > `optional` **clientContext?**: `string` Secure per-user client-safe context read (`GET /api/sdk/client-context`, plan 157). *** ### ingestSdkMeta > **ingestSdkMeta**: `string` Keyless anonymous SDK telemetry endpoint (`POST /api/sdk/meta`). *** ### ~~legacyInteractions?~~ > `optional` **legacyInteractions?**: `string` :::caution[Deprecated] Ignored since plan 232. This pointed at a fallback the SDK retried when the interactions POST failed — against a route the app does not serve, with the internal queue shape rather than the wire shape, so it could only ever 404. Setting it has no effect; the key remains so an existing integration that passes it still compiles. ::: *** ### surfaceSlots > **surfaceSlots**: `string` Optional sink for registered surface slots. RevTurbine hosts **no default route** for this verb (BL-0197). Slot registration is client-local and discovery is ingestion-driven — the pipeline derives discovered slots from the `slot_*` telemetry the SDK already emits through `/api/track`. Set this only when you run your own slot-inventory service; unset, `registerSurfaceSlot()` performs no network write. # RevTurbineEntitlementPlacementRequestOptions Optional context for entitlement-based placement requests. ## Properties ### ~~surfaceType?~~ > `optional` **surfaceType?**: `"inline"` \| `"custom"` \| `"push"` \| `"banner"` \| `"modal"` \| `"tooltip"` \| `"sidebar"` \| `"toast"` \| `"fullscreen"` \| `"email"` \| `"sms"` \| `"in_page"` \| `"button"` \| `"full_page"` \| `"agent"` \| `"cli"` :::caution[Deprecated] Use `componentType`. ::: # RevTurbineEntitlementRuleEvaluation Entitlement-rule evaluation detail for placement diagnostics. ## Properties ### outcomeDescription? > `optional` **outcomeDescription?**: `string` Human-readable description of what this rule grants (e.g. "grants access", "sets limit to 5,000 calls / month"). *** ### planScopes > **planScopes**: `string`[] Human-readable plan scopes resolved from `planIds` (e.g. `starter`, `professional`). *** ### segmentIds? > `optional` **segmentIds?**: `string`[] Segment IDs the rule is scoped to. Empty array means "matches all users". Evaluated with intra-dimension OR + cross-dimension AND per spec §2.5 (plan #39 REQ-8). # RevTurbineEventBatchingOptions Client-side clickstream batching policy (plan 95 TASK-6). ## Properties ### flushIntervalMs? > `optional` **flushIntervalMs?**: `number` Also flush the buffer on this interval, in milliseconds, so low-volume sessions don't strand events. Default `5000`. Set to `0` to disable the timer (size + page-unload flushing still apply). *** ### maxBatchSize? > `optional` **maxBatchSize?**: `number` Flush the buffer once this many events are queued. Default `20`. Clamped to a minimum of `1`. # RevTurbineInitStatus The provider's initialization outcome, readable with no SDK instance. `ok: true` with no other fields is the healthy state. On failure every field is populated — `remediation` is never empty, because a diagnostic that only restates the error leaves the reader exactly where they started. ## Properties ### message? > `readonly` `optional` **message?**: `string` The underlying error message, unmodified. Absent while healthy. *** ### ok > `readonly` **ok**: `boolean` `false` once initialization has thrown; `true` while healthy. *** ### phase? > `readonly` `optional` **phase?**: [`RevTurbineInitPhase`](/revturbine-sdk/api/type-aliases/revturbineinitphase/) Which phase threw. Absent while healthy. *** ### remediation? > `readonly` `optional` **remediation?**: `string` What to actually change, in the caller's own code. Absent while healthy. # RevTurbinePageContext Page-level context automatically inferred or manually set. ## Properties ### referrer? > `optional` **referrer?**: `string` HTTP referrer. *** ### tags? > `optional` **tags?**: `string`[] Semantic tags for page classification (e.g. `['pricing', 'upgrade']`). *** ### title? > `optional` **title?**: `string` Document title. *** ### url? > `optional` **url?**: `string` Fully qualified page URL. # RevTurbinePlacementBehaviorFlags Feature flags for placement pipeline behavior that may alter decision semantics. Each flag defaults to what the loaded Playbook authors (plan 174 TASK-2 / F-72): authored payload caps (`caps.max_per_period` / `caps.cooldown_days` or a `remind_later_minutes` window) turn on client caps enforcement; a `gated`-category placement turns on auto gated placement; a `trials`-category placement turns on trial auto-triggers. A Playbook that authors none of these leaves every flag `false` — existing integrations whose config authors nothing are unchanged. An explicit RevTurbineInitOptions.placementBehavior value (true or false) always overrides the derived default. ## Properties ### enableAutoGatedPlacement > **enableAutoGatedPlacement**: `boolean` Enable automatic gated placement rendering helpers on entitlement denial. *** ### enableClientCapsEnforcement > **enableClientCapsEnforcement**: `boolean` Enable client-side payload cap enforcement (max-per-period + cooldown precedence). *** ### enableTrialAutoTriggers > **enableTrialAutoTriggers**: `boolean` Enable automatic trial lifecycle trigger derivation in local-only mode. # RevTurbinePlacementConfig Configuration for registering a placement slot. The combination of name + route + scope key produces a deterministic placement ID. :::caution[Deprecated] Use `RevTurbineSurfaceSlotConfig`. ::: ## Properties ### ~~metadata?~~ > `optional` **metadata?**: [`SdkMetadata`](/revturbine-sdk/api/type-aliases/sdkmetadata/) Arbitrary metadata attached to the placement for analytics. *** ### ~~name~~ > **name**: `string` Human-readable placement name (e.g. `'pricing_banner'`). *** ### ~~placementScopeKey?~~ > `optional` **placementScopeKey?**: `string` Optional scope key to differentiate placements on the same route. # RevTurbinePlacementDecisionExplanation Structured explanation object for placement decision visualization. # RevTurbinePlacementPayloadEvaluation Placement payload eligibility detail for explainability UI. # RevTurbinePlacementRuleEvaluation Placement rule signals extracted from the selected placement output. # RevTurbinePlacementTypeEntity Tenant-scoped placement type metadata persisted by SDK integrations. # RevTurbinePlaybookLoadFailure Why the hosted Playbook did not load (BL-0012). A failed load used to look exactly like "no config yet": the fetch returned nothing, `can()` answered `config_unavailable`, and neither the console nor any API named the endpoint or the credential. Read it with `getPlaybookLoadFailure()`; the SDK also warns once per distinct failure while no Playbook is loaded. - `credentials` — no `publicKey` was supplied, so nothing can be fetched (an `apiKey` is the server key and is never sent from this SDK). - `bootstrap` / `manifest` / `bundle` — a step of the signed CDN delivery failed; the legacy `config` endpoint is tried next. - `config` — the legacy `GET /api/sdk/config` fetch failed too. ## Properties ### message > **message**: `string` A human-readable explanation, naming the credential when it is the likely cause. *** ### stage > **stage**: `"config"` \| `"credentials"` \| `"bootstrap"` \| `"manifest"` \| `"bundle"` Which step failed. *** ### status? > `optional` **status?**: `number` The HTTP status, when the step got a response. *** ### url? > `optional` **url?**: `string` The URL the step requested, when it made a request. # RevTurbinePolicySnapshot Snapshot of policy-related SDK runtime settings and loaded config metadata. ## Properties ### ~~exportedConfigVersion?~~ > `optional` **exportedConfigVersion?**: `string` :::caution[Deprecated] Renamed to [RevTurbinePolicySnapshot.playbookVersion](/revturbine-sdk/api/interfaces/revturbinepolicysnapshot/#playbookversion) (BL-0156). Still emitted with the identical value; removed in `0.13.0`. Read `playbookVersion`. ::: *** ### playbookVersion? > `optional` **playbookVersion?**: `string` `format_version` of the loaded Playbook, when the artifact declares one. # RevTurbineRegisteredSlot One surface slot this app currently has mounted. ## Properties ### internalId > **internalId**: `string` The SDK's internal registry id (hashed). Useful when correlating logs. *** ### name > **name**: `string` Display name supplied at registration. *** ### route > **route**: `string` Normalized route the slot was mounted on. *** ### slotId > **slotId**: `string` The author-facing slot id — what a Playbook placement's `trigger.slot_id` must equal. NOT the internal hashed registry key. *** ### surfaceTemplateIds > **surfaceTemplateIds**: `string`[] Surface template ids the slot declared it can render, if any. # RevTurbineRouteProps Props for [RevTurbineRoute](/revturbine-sdk/api/functions/revturbineroute/). ## Properties ### route > **route**: `string` \| `null` \| `undefined` The framework route pattern for this subtree — e.g. Next.js `/projects/[projectId]` (see `routePatternFromParams`) or a React Router pattern `/projects/:projectId`. Normalized before use: any query string, fragment or origin is dropped. `null`/`undefined` falls back to the browser's pathname. # RevTurbineSegmentEvaluation Segment-level evaluation detail for placement diagnostics. # RevTurbineSemanticEvent A named semantic event with structured payload. ## Properties ### eventType > **eventType**: `string` Event type name (e.g. `'checkout_started'`). *** ### options? > `optional` **options?**: `RevTurbineEventOptions` Delivery options. *** ### payload > **payload**: [`SdkEventProperties`](/revturbine-sdk/api/type-aliases/sdkeventproperties/) Structured event payload. # RevTurbineServerOptions Options for initializing the RevTurbine server-side SDK. ## Properties ### apiKey > **apiKey**: `string` Your **server key** — the secret backend credential (`rtk_…`, type `server`, minted under **Settings → API Token Management → Server Token**). Keep it in server-side configuration only. It is never the browser SDK's `publicKey`, and the control plane refuses it on browser-like requests. *** ### defaultTtlSeconds? > `optional` **defaultTtlSeconds?**: `number` Default TTL for evaluation payloads (seconds). Default: 60. *** ### endpoint > **endpoint**: `string` Base URL of the RevTurbine API Edge. *** ### fetch? > `optional` **fetch?**: (`input`, `init?`) => `Promise`\<`Response`\> Custom fetch implementation (e.g. for testing or non-standard runtimes). [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch) #### Parameters ##### input `RequestInfo` \| `URL` ##### init? `RequestInit` #### Returns `Promise`\<`Response`\> *** ### tenantId > **tenantId**: `string` Your RevTurbine tenant identifier. # RevTurbineSlotInventoryDiagnosis Result of [RevTurbineCustomerSdk.diagnoseSlotInventory](/revturbine-sdk/api/classes/revturbinecustomersdk/#diagnoseslotinventory). ## Properties ### authored > **authored**: [`RevTurbineAuthoredSlotRef`](/revturbine-sdk/api/interfaces/revturbineauthoredslotref/)[] Every slot-targeted placement in the Playbook. *** ### authoredButUnmounted > **authoredButUnmounted**: [`RevTurbineAuthoredSlotRef`](/revturbine-sdk/api/interfaces/revturbineauthoredslotref/)[] Placements targeting a slot no code renders. **These can never show**, and nothing else reports them — the decision path is never asked about a slot that was never mounted, so there is no error and no reason code. *** ### configAvailable > **configAvailable**: `boolean` Whether a Playbook was available to diff against. When `false`, `authored` is empty because nothing could be read — which is NOT the same as "nothing is authored", and a caller must not treat the two alike. *** ### mounted > **mounted**: [`RevTurbineRegisteredSlot`](/revturbine-sdk/api/interfaces/revturbineregisteredslot/)[] Every slot mounted by this app right now. *** ### mountedButUnauthored > **mountedButUnauthored**: [`RevTurbineRegisteredSlot`](/revturbine-sdk/api/interfaces/revturbineregisteredslot/)[] Slots mounted that no placement targets. They render their fallback forever; usually a typo in the slot id, or config that was never deployed. # RevTurbineSlotPlacementRequestOptions Optional context for slot-based placement requests. # RevTurbineSurfaceSlotConfig Canonical configuration for a renderable surface slot. `id` must be unique within the customer app integration. ## Properties ### id > **id**: `string` Required unique identifier for the surface slot. *** ### metadata? > `optional` **metadata?**: [`SdkMetadata`](/revturbine-sdk/api/type-aliases/sdkmetadata/) Arbitrary metadata attached to the slot for analytics and traceability. *** ### name? > `optional` **name?**: `string` Optional human-readable label used for analytics and debugging. *** ### surfaceTemplateIds? > `optional` **surfaceTemplateIds?**: `string`[] Surface template IDs that this slot accepts. When provided, only placements whose surface template matches one of these IDs are eligible to render in this slot. This acts as a filter at decision time — both the local resolver and the remote decide-context endpoint use this constraint. # RevTurbineTargeting Snapshot of the active targeting context used by placement eligibility. ## Extends - `UserTargetingContext` ## Properties ### configuredSegments > **configuredSegments**: `object`[] Segment definitions configured in the active RevTurbineConfig. *** ### configuredTraitFields > **configuredTraitFields**: `string`[] Predicate fields discovered from configured segments (targeting dimensions). # RevTurbineTelemetryOptions Behavior-telemetry configuration (plan 144). ## Properties ### consent? > `optional` **consent?**: [`TelemetryConsent`](/revturbine-sdk/api/type-aliases/telemetryconsent/) Consent gate for behavior telemetry. Defaults to `granted`. `denied` and `pending` both stop event creation, so no clickstream event reaches ingest, a registered RevTurbineEventConsumer, or an integration. Change it at runtime with [RevTurbineCustomerSdk.setTelemetryConsent](/revturbine-sdk/api/classes/revturbinecustomersdk/#settelemetryconsent) — no provider remount is needed (REQ-12). Independent of RevTurbineInitOptions.anonymousTelemetry, which governs the keyless SDK-init beacon (REQ-13). # RevTurbineTheme Complete SDK theme definition. All fields are optional when uploading — missing values fall back to defaults. ## Properties ### colors > **colors**: [`RevTurbineThemeColors`](/revturbine-sdk/api/interfaces/revturbinethemecolors/) Color tokens. *** ### id? > `optional` **id?**: `string` Theme identifier. *** ### name? > `optional` **name?**: `string` Human-readable theme name. *** ### shadows > **shadows**: [`RevTurbineThemeShadows`](/revturbine-sdk/api/interfaces/revturbinethemeshadows/) Shadow tokens. *** ### shape > **shape**: [`RevTurbineThemeShape`](/revturbine-sdk/api/interfaces/revturbinethemeshape/) Shape tokens. *** ### typography > **typography**: [`RevTurbineThemeTypography`](/revturbine-sdk/api/interfaces/revturbinethemetypography/) Typography tokens. *** ### version? > `optional` **version?**: `string` Monotonic version string for cache invalidation. # RevTurbineThemeColors Color tokens used across all placement components. ## Properties ### accent > **accent**: `string` Accent color (e.g. accent buttons). Default `#7c3aed`. *** ### accentText > **accentText**: `string` Text on accent backgrounds. Default `#ffffff`. *** ### background > **background**: `string` Page/card background. Default `#ffffff`. *** ### cliBackground > **cliBackground**: `string` CLI terminal background. Default `#1e1e1e`. *** ### cliLink > **cliLink**: `string` CLI link color. Default `#569cd6`. *** ### cliText > **cliText**: `string` CLI terminal text. Default `#d4d4d4`. *** ### danger > **danger**: `string` Danger state color (quota >90%). Default `#dc2626`. *** ### info > **info**: `string` Informational/link color. Default `#60a5fa`. *** ### overlay > **overlay**: `string` Overlay backdrop color. Default `rgba(0,0,0,0.5)`. *** ### primary > **primary**: `string` Primary brand color (buttons, banners, progress fills). Default `#1e40af`. *** ### primaryText > **primaryText**: `string` Text color on primary backgrounds. Default `#ffffff`. *** ### secondary > **secondary**: `string` Secondary/surface background. Default `#f3f4f6`. *** ### secondaryText > **secondaryText**: `string` Text on secondary backgrounds. Default `#1f2937`. *** ### success > **success**: `string` Success state color. Default `#16a34a`. *** ### surface > **surface**: `string` Surface/card background (e.g. inline embed). Default `#f8fafc`. *** ### surfaceBorder > **surfaceBorder**: `string` Surface border color. Default `#e2e8f0`. *** ### text > **text**: `string` Primary text color. Default `#111827`. *** ### textMuted > **textMuted**: `string` Muted/hint text color. Default `#6b7280`. *** ### textSecondary > **textSecondary**: `string` Secondary text color. Default `#4b5563`. *** ### toastBackground > **toastBackground**: `string` Toast/dark surface background. Default `#1f2937`. *** ### toastText > **toastText**: `string` Toast text color. Default `#ffffff`. *** ### track > **track**: `string` Track/unfilled bar background. Default `#e5e7eb`. *** ### warning > **warning**: `string` Warning state color (quota 70-90%). Default `#f59e0b`. # RevTurbineThemeShadows Shadow tokens. ## Properties ### large > **large**: `string` Modal dialog shadow. Default `0 20px 60px rgba(0,0,0,0.3)`. *** ### medium > **medium**: `string` Toast/floating element shadow. Default `0 10px 40px rgba(0,0,0,0.25)`. # RevTurbineThemeShape Shape/radius tokens. ## Properties ### borderRadius > **borderRadius**: `string` Default border radius. Default `8px`. *** ### borderRadiusLarge > **borderRadiusLarge**: `string` Large border radius (modal dialog). Default `12px`. *** ### borderRadiusSmall > **borderRadiusSmall**: `string` Small border radius. Default `6px`. # RevTurbineThemeTypography Typography tokens. ## Properties ### fontFamily > **fontFamily**: `string` Base font family. Default `system-ui, -apple-system, sans-serif`. *** ### fontFamilyMono > **fontFamilyMono**: `string` Monospace font family for CLI. Default `ui-monospace, SFMono-Regular, ...`. *** ### fontSize > **fontSize**: `string` Base font size. Default `14px`. *** ### fontSizeHeader > **fontSizeHeader**: `string` Header font size. Default `20px`. *** ### fontSizeLargeHeader > **fontSizeLargeHeader**: `string` Large header font size (full page). Default `28px`. *** ### fontSizeSmall > **fontSizeSmall**: `string` Small font size (CTA buttons, labels). Default `13px`. # RevTurbineTreatmentInteractionOptions Optional fields for constructing typed interaction payloads. # RevTurbineUsageSnapshotEntry Usage snapshot entry for a usage unit. ## Properties ### current > **current**: `number` Current consumed amount for the usage unit. *** ### limit? > `optional` **limit?**: `number` Configured limit for the usage unit when available. # RevTurbineUserContext SDK-local user context state. Extends the canonical [UserContextInput](/revturbine-sdk/api/type-aliases/usercontextinput/) with a local `id` field for user identification. The full persistence-ready UserContext (with `tenant_id`, `user_id`, timestamps) is built by the SDK via [getUserContext()](/revturbine-sdk/api/classes/revturbinecustomersdk/#getusercontext). `custom` is widened to `Record` at the SDK boundary for customer convenience; values are narrowed to `TraitValue` when persisted via the API. `seat_type_handle` is omitted on purpose (plan 279, D-36). A user's seat type is assigned only server-side: by the tenant's backend through a server-key user-context upsert, or by RevTurbine's enrichment in hosted mode. The browser never writes it, so neither `identify()`, `update()`, `setUserContext()` nor the `user` init option accepts it. The browser receives the resolved `seat_type` / `buyer_role` as built-in dimensions delivered by `GET /api/sdk/client-context` instead. The server-computed `activity_score` / `activity_score_computed_at` are omitted on the same principle (BL-0352); the browser receives the derived `activity_level` dimension. `builtin_dimensions` is omitted too (D-46, BL-0381): the browser never asserts a built-in dimension value. The SDK holds the values `fetchClientContext` delivers and stamps the reserved `rt_` segment traits from them; read them back through [getUserContext()](/revturbine-sdk/api/classes/revturbinecustomersdk/#getusercontext). A plain-JS caller that passes any of these keys has them dropped and reported at runtime. ## Extends - `Omit`\<[`UserContextInput`](/revturbine-sdk/api/type-aliases/usercontextinput/), `"custom"` \| `"entitlements"` \| `"usage"` \| `"personalization"` \| `"derived_computed_at"` \| `"context_hash"` \| `ServerAssignedUserContextKey`\> ## Properties ### context\_hash? > `optional` **context\_hash?**: `string` \| `null` Server-computed user-context hash (plan 74). Optional at the SDK input boundary for the same reason as `derived_computed_at`. *** ### custom? > `optional` **custom?**: [`SdkTraits`](/revturbine-sdk/api/type-aliases/sdktraits/) Customer-defined fields for segmentation and personalization. *** ### derived\_computed\_at? > `optional` **derived\_computed\_at?**: `string` \| `null` Server-computed derived-entitlement cache stamp (plan 74). Optional at the SDK input boundary — local mode does not compute it; the control plane populates and round-trips it, defaulting to `null` when absent. *** ### entitlements? > `optional` **entitlements?**: `Record`\<`string`, `boolean` \| \{ \}\> App-supplied entitlement data, keyed by handle — a boolean or a grant (`status`, `limit`, `used`). This is Entitlement Mirroring (D-61): it merges with the Playbook evaluation, and by default the app's data wins. Browser-supplied data shapes the UI only; the server re-check on the same inputs stays authoritative for paid actions. *** ### id? > `optional` **id?**: `string` Authenticated user identifier. When undefined, the SDK uses an anonymous ID. *** ### personalization? > `optional` **personalization?**: [`SdkTraits`](/revturbine-sdk/api/type-aliases/sdktraits/) Transient personalization token map. Holds SDK-derived tokens (plan_name, usage_current, etc.) merged with app-provided tokens. Not persisted — rebuilt on each SDK session. Widened to accept `unknown` at the SDK boundary; narrowed to `string | number` when serialized. *** ### usage? > `optional` **usage?**: `Record`\<`string`, \{ \}\> Usage entries derived from credits / usage_limit entitlements, keyed by handle. # ServerActionContext Context supplied to an app-owned server mutation handler. ## Properties ### actionType > **actionType**: `string` The authored CTA action type (v1 documents `extend_trial`). *** ### params > **params**: `Record`\<`string`, `unknown`\> Tenant-authored custom CTA parameters, when present. *** ### placement > **placement**: `object` The placement whose CTA initiated the action. # ServerActionResult Result returned after the app's backend accepts or refuses the mutation. ## Properties ### userContext? > `optional` **userContext?**: [`UserContextInput`](/revturbine-sdk/api/type-aliases/usercontextinput/) Fresh server-authoritative context to merge after a successful mutation. # ServerEvaluationRequest Full evaluation request submitted to the server SDK. ## Properties ### anonymousId? > `optional` **anonymousId?**: `string` Optional anonymous ID for correlation (generated server-side when omitted). *** ### entitlementHandles? > `optional` **entitlementHandles?**: `string`[] Entitlement handles to check. *** ### includeTheme? > `optional` **includeTheme?**: `boolean` Whether to include the tenant theme in the payload. *** ### includeTrialStatus? > `optional` **includeTrialStatus?**: `boolean` Whether to include trial status in the payload. *** ### includeUserContext? > `optional` **includeUserContext?**: `boolean` Whether to include full user context (segments, traits, balances). *** ### page? > `optional` **page?**: `object` Page context when rendering a specific page server-side. #### tags? > `optional` **tags?**: `string`[] #### title? > `optional` **title?**: `string` #### url? > `optional` **url?**: `string` *** ### placements? > `optional` **placements?**: [`ServerPlacementRequest`](/revturbine-sdk/api/interfaces/serverplacementrequest/)[] Placements to evaluate. When omitted, evaluates the bootstrap context. *** ### traits? > `optional` **traits?**: `Record`\<`string`, `unknown`\> User traits for segmentation/personalization. *** ### usageBalances? > `optional` **usageBalances?**: `Record`\<`string`, `number`\> Current usage balances keyed by entitlement handle. *** ### userId > **userId**: `string` Authenticated user identifier. # ServerPlacementRequest A single placement to evaluate on the server. ## Properties ### entitlementHandle? > `optional` **entitlementHandle?**: `string` Entitlement handle for entitlement-gated decisions. *** ### placementHandle? > `optional` **placementHandle?**: `string` Placement handle for chained CTA paths. *** ### planHandle? > `optional` **planHandle?**: `string` Plan handle for plan-specific placements. *** ### slotId? > `optional` **slotId?**: `string` Slot identifier for slot-based decisions. # ServerUserContextSnapshot The client-safe, server-known signals surfaced as `traits:server` traits. Sourced from the mapped `/api/sdk/client-context` response. ## Properties ### payment\_at\_risk? > `optional` **payment\_at\_risk?**: `boolean` Coarse billing-recovery signal (raw failed-payment detail never crosses the boundary). *** ### trial? > `optional` **trial?**: `object` Trial signals the control plane owns. #### days\_remaining? > `optional` **days\_remaining?**: `number` #### in\_trial? > `optional` **in\_trial?**: `boolean` #### state? > `optional` **state?**: `string` # SetBuiltinDimensionsOptions Options for [RevTurbineServer.setBuiltinDimensions](/revturbine-sdk/api/classes/revturbineserver/#setbuiltindimensions). ## Properties ### override? > `optional` **override?**: `boolean` `false` (default) — an UPDATE: the value is delivered until a newer RevTurbine enrichment change (Stripe subscription / trial / billing, the activity job, request signals) supersedes it — last writer wins, enrichment included. Writing a pinned dimension without the flag UNPINS it. `true` — an OVERRIDE: the values written in this call are pinned, and RevTurbine enrichment never replaces them until you write that dimension again without the flag, or clear it with `null` (ruling D-47). # TelemetryScopeProps Props for [TelemetryScope](/revturbine-sdk/api/functions/telemetryscope/). ## Extends - [`TelemetryScopeValue`](/revturbine-sdk/api/interfaces/telemetryscopevalue/) ## Properties ### action? > `optional` **action?**: `string` Default action label for events in this scope. #### Inherited from [`TelemetryScopeValue`](/revturbine-sdk/api/interfaces/telemetryscopevalue/).[`action`](/revturbine-sdk/api/interfaces/telemetryscopevalue/#action) *** ### area? > `optional` **area?**: `string` Logical product area (e.g. `'billing'`, `'editor'`). #### Inherited from [`TelemetryScopeValue`](/revturbine-sdk/api/interfaces/telemetryscopevalue/).[`area`](/revturbine-sdk/api/interfaces/telemetryscopevalue/#area) *** ### purpose? > `optional` **purpose?**: [`TrackPurpose`](/revturbine-sdk/api/type-aliases/trackpurpose/) Advisory [TrackPurpose](/revturbine-sdk/api/type-aliases/trackpurpose/). #### Inherited from [`TelemetryScopeValue`](/revturbine-sdk/api/interfaces/telemetryscopevalue/).[`purpose`](/revturbine-sdk/api/interfaces/telemetryscopevalue/#purpose) # TelemetryScopeValue Ambient telemetry context established by [TelemetryScope](/revturbine-sdk/api/functions/telemetryscope/) and merged into every event emitted by [useTrack](/revturbine-sdk/api/functions/usetrack/) beneath it. ## Extended by - [`TelemetryScopeProps`](/revturbine-sdk/api/interfaces/telemetryscopeprops/) ## Properties ### action? > `optional` **action?**: `string` Default action label for events in this scope. *** ### area? > `optional` **area?**: `string` Logical product area (e.g. `'billing'`, `'editor'`). *** ### purpose? > `optional` **purpose?**: [`TrackPurpose`](/revturbine-sdk/api/type-aliases/trackpurpose/) Advisory [TrackPurpose](/revturbine-sdk/api/type-aliases/trackpurpose/). # TrackedAction State + runner returned by [useTrackedAction](/revturbine-sdk/api/functions/usetrackedaction/). ## Type Parameters ### A `A` *extends* `unknown`[] ### R `R` ## Properties ### error > **error**: [`ActionErrorCategory`](/revturbine-sdk/api/type-aliases/actionerrorcategory/) \| `null` The last failure's normalized category, or `null`. *** ### isRunning > **isRunning**: `boolean` True while the action is in flight. *** ### run > **run**: (...`args`) => `Promise`\<`R`\> Run the action: emits `${name}_started`, then `${name}_completed` or `${name}_failed`. Preserves the return value and re-throws. #### Parameters ##### args ...`A` #### Returns `Promise`\<`R`\> # TrackOnViewProps Props for [TrackOnView](/revturbine-sdk/api/functions/trackonview/). ## Properties ### as? > `optional` **as?**: `string` \| `number` \| `symbol` Host element tag. Default `'div'`. *** ### data? > `optional` **data?**: [`SdkEventProperties`](/revturbine-sdk/api/type-aliases/sdkeventproperties/) Event data (reserved names are dropped by [useTrack](/revturbine-sdk/api/functions/usetrack/)). *** ### event > **event**: `string` Event name emitted once when the element is qualified-viewed. *** ### minVisibleMs? > `optional` **minVisibleMs?**: `number` Minimum visible dwell before it counts, in ms. Default 0. *** ### options? > `optional` **options?**: [`TrackOptions`](/revturbine-sdk/api/interfaces/trackoptions/) Track options (area / action / purpose / immediate). *** ### threshold? > `optional` **threshold?**: `number` Visible fraction required (0–1). Default 0.5. # TrackOptions Per-event options for [useTrack](/revturbine-sdk/api/functions/usetrack/) — the one shared shape scope defaults and later telemetry components also draw from (plan 144 TASK-12). ## Properties ### action? > `optional` **action?**: `string` Overrides the scope `action` for this event. *** ### area? > `optional` **area?**: `string` Overrides the scope `area` for this event. *** ### dedupeKey? > `optional` **dedupeKey?**: `string` Emit at most once per distinct key, for this hook instance's lifetime. *** ### immediate? > `optional` **immediate?**: `boolean` Send immediately instead of batching. *** ### once? > `optional` **once?**: `boolean` Emit at most once (keyed on the event name), for this hook instance's lifetime. *** ### purpose? > `optional` **purpose?**: [`TrackPurpose`](/revturbine-sdk/api/type-aliases/trackpurpose/) Advisory [TrackPurpose](/revturbine-sdk/api/type-aliases/trackpurpose/); overrides the scope purpose. # TrackProps Props for [Track](/revturbine-sdk/api/functions/track/). ## Properties ### as? > `optional` **as?**: `string` \| `number` \| `symbol` Host element tag when not `asChild`. Default `'span'`. *** ### asChild? > `optional` **asChild?**: `boolean` Compose the telemetry onto the **single child element** instead of rendering a wrapper (Radix-Slot style). Preserves the child's accessible name, disabled state, and existing `onClick`. *** ### data? > `optional` **data?**: [`SdkEventProperties`](/revturbine-sdk/api/type-aliases/sdkeventproperties/) Event data (reserved names are dropped by [useTrack](/revturbine-sdk/api/functions/usetrack/)). *** ### event > **event**: `string` Event name to emit on click. *** ### options? > `optional` **options?**: [`TrackOptions`](/revturbine-sdk/api/interfaces/trackoptions/) Track options (area / action / purpose / once / dedupeKey / immediate). # TrialTriggerPayload Payload for trial-related trigger events. # UsageTriggerPayload Payload for usage/limit trigger events. ## Properties ### current\_usage? > `optional` **current\_usage?**: `number` Current usage count for the entitlement. *** ### entitlement\_handle? > `optional` **entitlement\_handle?**: `string` The entitlement handle this usage event refers to. *** ### usage\_limit? > `optional` **usage\_limit?**: `number` Usage limit configured for the entitlement on the active plan. # UseCanResult Result of [useCan](/revturbine-sdk/api/functions/usecan/) — the entitlement check reduced to the shape a `can` question actually needs. ## Properties ### can > **can**: `boolean` `true` when the user may proceed — the evaluator's verdict, i.e. not `denied`. A `limited` result still grants access when the evaluator allows it (degrade mode / running low), but an at-cap limit whose enforcement blocks — including the unset-enforcement default — resolves `limited` with `allowed: false` and `can` is `false`. Deny-until-ready, matching the SDK's fail-closed contract: `can` is `false` until the check resolves, and stays `false` if the check errors. Evaluation is local to the loaded Playbook (no per-check network call), so the unresolved window is one microtask once initialization completes; in hosted mode the first load spans the initial config fetch. Gate paywall UI on `!can && !isLoading` so entitled users never see an upsell flash; never gate on `!allowed` (that would also block `limited` users who are still entitled). *** ### isLoading > **isLoading**: `boolean` `true` until the entitlement check first resolves (including while the SDK initializes), and again while a recheck is in flight. Settles to `false` once a result — or an error — has landed. Pair with `can`: `!can && isLoading` is "still deciding"; `!can && !isLoading` is a settled deny. *** ### limited > **limited**: `boolean` `true` when access is granted but the balance is approaching its limit (the "running low" state). Surface a soft warning while still allowing the action. *** ### result > **result**: \{ \} \| `null` The full entitlement result (`status`, `reason`, `current_tier`, `placement`, …), or `null` until the check resolves — the escape hatch for details beyond `can` / `limited`. # UsePlacementPersonalizationOptions Options for deriving placement personalization tokens from provider state. ## Properties ### personalization? > `optional` **personalization?**: [`PersonalizationContext`](/revturbine-sdk/api/interfaces/personalizationcontext/) Optional base personalization values merged before derived provider tokens. *** ### refreshKey? > `optional` **refreshKey?**: `string` \| `number` Optional dependency trigger for re-resolving provider state. *** ### userId? > `optional` **userId?**: `string` Optional explicit user ID. Falls back to provider default user ID when omitted. # UserProfileProps Props for [UserProfile](/revturbine-sdk/api/functions/userprofile/). ## Properties ### className? > `optional` **className?**: `string` Optional class name for root container. *** ### style? > `optional` **style?**: `any` Optional inline styles for root container. *** ### targeting > **targeting**: [`RevTurbineTargeting`](/revturbine-sdk/api/interfaces/revturbinetargeting/) Targeting/user context snapshot to render. *** ### title? > `optional` **title?**: `string` Optional heading override. # @revt-eng/sdk ## Classes - [BrowserStorage](/revturbine-sdk/api/classes/browserstorage/) - [CtaResolverRegistry](/revturbine-sdk/api/classes/ctaresolverregistry/) - [PlacementController](/revturbine-sdk/api/classes/placementcontroller/) - [PlacementTypeRegistry](/revturbine-sdk/api/classes/placementtyperegistry/) - [RevTurbineApiError](/revturbine-sdk/api/classes/revturbineapierror/) - [RevTurbineBuiltinDimensionsError](/revturbine-sdk/api/classes/revturbinebuiltindimensionserror/) - [RevTurbineClientSessionError](/revturbine-sdk/api/classes/revturbineclientsessionerror/) - [RevTurbineCustomerSdk](/revturbine-sdk/api/classes/revturbinecustomersdk/) - [RevTurbineDimensionWriteError](/revturbine-sdk/api/classes/revturbinedimensionwriteerror/) - [RevTurbineSeatAssignmentError](/revturbine-sdk/api/classes/revturbineseatassignmenterror/) - [RevTurbineServer](/revturbine-sdk/api/classes/revturbineserver/) - [SdkSession](/revturbine-sdk/api/classes/sdksession/) - [ServerUserContextProvider](/revturbine-sdk/api/classes/serverusercontextprovider/) - [UnsupportedExperimentAssignmentUnitError](/revturbine-sdk/api/classes/unsupportedexperimentassignmentuniterror/) ## Interfaces - [AnalyticsProviderOptions](/revturbine-sdk/api/interfaces/analyticsprovideroptions/) - [AssignSeatTypeOptions](/revturbine-sdk/api/interfaces/assignseattypeoptions/) - [BasicBucketerExperiment](/revturbine-sdk/api/interfaces/basicbucketerexperiment/) - [BrandingResolutionInput](/revturbine-sdk/api/interfaces/brandingresolutioninput/) - [CanonicalExperimentAllocation](/revturbine-sdk/api/interfaces/canonicalexperimentallocation/) - [ClientSessionResult](/revturbine-sdk/api/interfaces/clientsessionresult/) - [CompositeExperimentProviderOptions](/revturbine-sdk/api/interfaces/compositeexperimentprovideroptions/) - [ConfigTargetDefaults](/revturbine-sdk/api/interfaces/configtargetdefaults/) - [ControlPlaneEmitInput](/revturbine-sdk/api/interfaces/controlplaneemitinput/) - [CreateClientSessionInput](/revturbine-sdk/api/interfaces/createclientsessioninput/) - [CtaResolverContext](/revturbine-sdk/api/interfaces/ctaresolvercontext/) - [EligibleAddon](/revturbine-sdk/api/interfaces/eligibleaddon/) - [EligiblePlan](/revturbine-sdk/api/interfaces/eligibleplan/) - [EligiblePrice](/revturbine-sdk/api/interfaces/eligibleprice/) - [EngagementAreaProps](/revturbine-sdk/api/interfaces/engagementareaprops/) - [EntitlementGateOptions](/revturbine-sdk/api/interfaces/entitlementgateoptions/) - [EntitlementGateState](/revturbine-sdk/api/interfaces/entitlementgatestate/) - [ExperimentAssignmentFactDeclaration](/revturbine-sdk/api/interfaces/experimentassignmentfactdeclaration/) - [ExperimentAssignmentFactMetadataCarrier](/revturbine-sdk/api/interfaces/experimentassignmentfactmetadatacarrier/) - [FeatureGateTriggerPayload](/revturbine-sdk/api/interfaces/featuregatetriggerpayload/) - [GatedAction](/revturbine-sdk/api/interfaces/gatedaction/) - [InitFailureDiagnosticProps](/revturbine-sdk/api/interfaces/initfailurediagnosticprops/) - [NativeExperimentAssignmentOptions](/revturbine-sdk/api/interfaces/nativeexperimentassignmentoptions/) - [PaymentTriggerPayload](/revturbine-sdk/api/interfaces/paymenttriggerpayload/) - [PersonalizationContext](/revturbine-sdk/api/interfaces/personalizationcontext/) - [PlacementContentFields](/revturbine-sdk/api/interfaces/placementcontentfields/) - [PlacementControllerOptions](/revturbine-sdk/api/interfaces/placementcontrolleroptions/) - [PlacementControllerState](/revturbine-sdk/api/interfaces/placementcontrollerstate/) - [PlacementCustomCode](/revturbine-sdk/api/interfaces/placementcustomcode/) - [PlacementDecisionInspectorProps](/revturbine-sdk/api/interfaces/placementdecisioninspectorprops/) - [PlacementPreviewConfig](/revturbine-sdk/api/interfaces/placementpreviewconfig/) - [PlacementSlotProps](/revturbine-sdk/api/interfaces/placementslotprops/) - [PlacementSlotType](/revturbine-sdk/api/interfaces/placementslottype/) - [PlacementUiPath](/revturbine-sdk/api/interfaces/placementuipath/) - [PostHogAnalyticsProviderOptions](/revturbine-sdk/api/interfaces/posthoganalyticsprovideroptions/) - [PostHogIntegrationOptions](/revturbine-sdk/api/interfaces/posthogintegrationoptions/) - [PostHogLike](/revturbine-sdk/api/interfaces/posthoglike/) - [ResolvedBranding](/revturbine-sdk/api/interfaces/resolvedbranding/) - [RevTurbineApiPaths](/revturbine-sdk/api/interfaces/revturbineapipaths/) - [RevTurbineAuthoredSlotRef](/revturbine-sdk/api/interfaces/revturbineauthoredslotref/) - [RevTurbineChainedPlacementRequestOptions](/revturbine-sdk/api/interfaces/revturbinechainedplacementrequestoptions/) - [RevTurbineCheckoutMetadata](/revturbine-sdk/api/interfaces/revturbinecheckoutmetadata/) - [RevTurbineCheckoutMetadataBag](/revturbine-sdk/api/interfaces/revturbinecheckoutmetadatabag/) - [RevTurbineConfigProvider](/revturbine-sdk/api/interfaces/revturbineconfigprovider/) - [RevTurbineContextPolicy](/revturbine-sdk/api/interfaces/revturbinecontextpolicy/) - [RevTurbineEndpointOverrides](/revturbine-sdk/api/interfaces/revturbineendpointoverrides/) - [RevTurbineEntitlementPlacementRequestOptions](/revturbine-sdk/api/interfaces/revturbineentitlementplacementrequestoptions/) - [RevTurbineEntitlementRuleEvaluation](/revturbine-sdk/api/interfaces/revturbineentitlementruleevaluation/) - [RevTurbineEventBatchingOptions](/revturbine-sdk/api/interfaces/revturbineeventbatchingoptions/) - [RevTurbineInitStatus](/revturbine-sdk/api/interfaces/revturbineinitstatus/) - [RevTurbinePageContext](/revturbine-sdk/api/interfaces/revturbinepagecontext/) - [RevTurbinePlacementBehaviorFlags](/revturbine-sdk/api/interfaces/revturbineplacementbehaviorflags/) - [~~RevTurbinePlacementConfig~~](/revturbine-sdk/api/interfaces/revturbineplacementconfig/) - [RevTurbinePlacementDecisionExplanation](/revturbine-sdk/api/interfaces/revturbineplacementdecisionexplanation/) - [RevTurbinePlacementPayloadEvaluation](/revturbine-sdk/api/interfaces/revturbineplacementpayloadevaluation/) - [RevTurbinePlacementRuleEvaluation](/revturbine-sdk/api/interfaces/revturbineplacementruleevaluation/) - [RevTurbinePlacementTypeEntity](/revturbine-sdk/api/interfaces/revturbineplacementtypeentity/) - [RevTurbinePlaybookLoadFailure](/revturbine-sdk/api/interfaces/revturbineplaybookloadfailure/) - [RevTurbinePolicySnapshot](/revturbine-sdk/api/interfaces/revturbinepolicysnapshot/) - [RevTurbineRegisteredSlot](/revturbine-sdk/api/interfaces/revturbineregisteredslot/) - [RevTurbineRouteProps](/revturbine-sdk/api/interfaces/revturbinerouteprops/) - [RevTurbineSegmentEvaluation](/revturbine-sdk/api/interfaces/revturbinesegmentevaluation/) - [RevTurbineSemanticEvent](/revturbine-sdk/api/interfaces/revturbinesemanticevent/) - [RevTurbineServerOptions](/revturbine-sdk/api/interfaces/revturbineserveroptions/) - [RevTurbineSlotInventoryDiagnosis](/revturbine-sdk/api/interfaces/revturbineslotinventorydiagnosis/) - [RevTurbineSlotPlacementRequestOptions](/revturbine-sdk/api/interfaces/revturbineslotplacementrequestoptions/) - [RevTurbineSurfaceSlotConfig](/revturbine-sdk/api/interfaces/revturbinesurfaceslotconfig/) - [RevTurbineTargeting](/revturbine-sdk/api/interfaces/revturbinetargeting/) - [RevTurbineTelemetryOptions](/revturbine-sdk/api/interfaces/revturbinetelemetryoptions/) - [RevTurbineTheme](/revturbine-sdk/api/interfaces/revturbinetheme/) - [RevTurbineThemeColors](/revturbine-sdk/api/interfaces/revturbinethemecolors/) - [RevTurbineThemeShadows](/revturbine-sdk/api/interfaces/revturbinethemeshadows/) - [RevTurbineThemeShape](/revturbine-sdk/api/interfaces/revturbinethemeshape/) - [RevTurbineThemeTypography](/revturbine-sdk/api/interfaces/revturbinethemetypography/) - [RevTurbineTreatmentInteractionOptions](/revturbine-sdk/api/interfaces/revturbinetreatmentinteractionoptions/) - [RevTurbineUsageSnapshotEntry](/revturbine-sdk/api/interfaces/revturbineusagesnapshotentry/) - [RevTurbineUserContext](/revturbine-sdk/api/interfaces/revturbineusercontext/) - [ServerActionContext](/revturbine-sdk/api/interfaces/serveractioncontext/) - [ServerActionResult](/revturbine-sdk/api/interfaces/serveractionresult/) - [ServerEvaluationRequest](/revturbine-sdk/api/interfaces/serverevaluationrequest/) - [ServerPlacementRequest](/revturbine-sdk/api/interfaces/serverplacementrequest/) - [ServerUserContextSnapshot](/revturbine-sdk/api/interfaces/serverusercontextsnapshot/) - [SetBuiltinDimensionsOptions](/revturbine-sdk/api/interfaces/setbuiltindimensionsoptions/) - [TelemetryScopeProps](/revturbine-sdk/api/interfaces/telemetryscopeprops/) - [TelemetryScopeValue](/revturbine-sdk/api/interfaces/telemetryscopevalue/) - [TrackedAction](/revturbine-sdk/api/interfaces/trackedaction/) - [TrackOnViewProps](/revturbine-sdk/api/interfaces/trackonviewprops/) - [TrackOptions](/revturbine-sdk/api/interfaces/trackoptions/) - [TrackProps](/revturbine-sdk/api/interfaces/trackprops/) - [TrialTriggerPayload](/revturbine-sdk/api/interfaces/trialtriggerpayload/) - [UsageTriggerPayload](/revturbine-sdk/api/interfaces/usagetriggerpayload/) - [UseCanResult](/revturbine-sdk/api/interfaces/usecanresult/) - [UsePlacementPersonalizationOptions](/revturbine-sdk/api/interfaces/useplacementpersonalizationoptions/) - [UserProfileProps](/revturbine-sdk/api/interfaces/userprofileprops/) ## Type Aliases - [ActionErrorCategory](/revturbine-sdk/api/type-aliases/actionerrorcategory/) - [AnalyticsEventHandler](/revturbine-sdk/api/type-aliases/analyticseventhandler/) - [AnalyticsEventProperties](/revturbine-sdk/api/type-aliases/analyticseventproperties/) - [AnalyticsEventTransformer](/revturbine-sdk/api/type-aliases/analyticseventtransformer/) - [AssignmentFactUnit](/revturbine-sdk/api/type-aliases/assignmentfactunit/) - [BannerComponentProps](/revturbine-sdk/api/type-aliases/bannercomponentprops/) - [BrandingSource](/revturbine-sdk/api/type-aliases/brandingsource/) - [BrowserRuntimeOptions](/revturbine-sdk/api/type-aliases/browserruntimeoptions/) - [BuiltinDimensionsErrorReason](/revturbine-sdk/api/type-aliases/builtindimensionserrorreason/) - [ButtonComponentProps](/revturbine-sdk/api/type-aliases/buttoncomponentprops/) - [ChangeListener](/revturbine-sdk/api/type-aliases/changelistener/) - [CliComponentProps](/revturbine-sdk/api/type-aliases/clicomponentprops/) - [ConfigArtifact](/revturbine-sdk/api/type-aliases/configartifact/) - [CreditBalanceComponentProps](/revturbine-sdk/api/type-aliases/creditbalancecomponentprops/) - [CtaResolver](/revturbine-sdk/api/type-aliases/ctaresolver/) - [DimensionWriteErrorReason](/revturbine-sdk/api/type-aliases/dimensionwriteerrorreason/) - [EmittablePlatformEventName](/revturbine-sdk/api/type-aliases/emittableplatformeventname/) - [EntitlementStatus](/revturbine-sdk/api/type-aliases/entitlementstatus/) - [ExactInitOptions](/revturbine-sdk/api/type-aliases/exactinitoptions/) - [~~ExportedConfigProvider~~](/revturbine-sdk/api/type-aliases/exportedconfigprovider/) - [FullPageComponentProps](/revturbine-sdk/api/type-aliases/fullpagecomponentprops/) - [IdentifyContextInput](/revturbine-sdk/api/type-aliases/identifycontextinput/) - [InlineEmbedComponentProps](/revturbine-sdk/api/type-aliases/inlineembedcomponentprops/) - [InPageComponentProps](/revturbine-sdk/api/type-aliases/inpagecomponentprops/) - [~~LegacyConfigTargetDefaults~~](/revturbine-sdk/api/type-aliases/legacyconfigtargetdefaults/) - [MessageTriggerType](/revturbine-sdk/api/type-aliases/messagetriggertype/) - [ModalComponentProps](/revturbine-sdk/api/type-aliases/modalcomponentprops/) - [ModalLayout](/revturbine-sdk/api/type-aliases/modallayout/) - [NativeExperimentAssignmentUnit](/revturbine-sdk/api/type-aliases/nativeexperimentassignmentunit/) - [PlacementPromotion](/revturbine-sdk/api/type-aliases/placementpromotion/) - [PlacementProps](/revturbine-sdk/api/type-aliases/placementprops/) - [PlacementUiPathActionType](/revturbine-sdk/api/type-aliases/placementuipathactiontype/) - [PlaybookProvider](/revturbine-sdk/api/type-aliases/playbookprovider/) - [QuotaMeterComponentProps](/revturbine-sdk/api/type-aliases/quotametercomponentprops/) - [RecognizedIdentifyKey](/revturbine-sdk/api/type-aliases/recognizedidentifykey/) - [RecognizedUpdateKey](/revturbine-sdk/api/type-aliases/recognizedupdatekey/) - [RegisterPlacementSlotTypeOptions](/revturbine-sdk/api/type-aliases/registerplacementslottypeoptions/) - [ResolvedContent](/revturbine-sdk/api/type-aliases/resolvedcontent/) - [RevTurbineClientSessionProvider](/revturbine-sdk/api/type-aliases/revturbineclientsessionprovider/) - [RevTurbineColorScheme](/revturbine-sdk/api/type-aliases/revturbinecolorscheme/) - [RevTurbineConfigProviderInput](/revturbine-sdk/api/type-aliases/revturbineconfigproviderinput/) - [RevTurbineGateResult](/revturbine-sdk/api/type-aliases/revturbinegateresult/) - [RevTurbineImpressionMetadata](/revturbine-sdk/api/type-aliases/revturbineimpressionmetadata/) - [RevTurbineInitBaseOptions](/revturbine-sdk/api/type-aliases/revturbineinitbaseoptions/) - [RevTurbineInitInputOptions](/revturbine-sdk/api/type-aliases/revturbineinitinputoptions/) - [RevTurbineInitOptionsStrict](/revturbine-sdk/api/type-aliases/revturbineinitoptionsstrict/) - [RevTurbineInitPhase](/revturbine-sdk/api/type-aliases/revturbineinitphase/) - [RevTurbineLocalOnlyMinimalInitOptions](/revturbine-sdk/api/type-aliases/revturbinelocalonlyminimalinitoptions/) - [RevTurbinePlaybookLoadState](/revturbine-sdk/api/type-aliases/revturbineplaybookloadstate/) - [RevTurbineRequiredUiPathResolvers](/revturbine-sdk/api/type-aliases/revturbinerequireduipathresolvers/) - [RevTurbineRuntimeMode](/revturbine-sdk/api/type-aliases/revturbineruntimemode/) - [RevTurbineSdkMode](/revturbine-sdk/api/type-aliases/revturbinesdkmode/) - [RevTurbineSegmentPredicateEvaluation](/revturbine-sdk/api/type-aliases/revturbinesegmentpredicateevaluation/) - [RevTurbineThemeInput](/revturbine-sdk/api/type-aliases/revturbinethemeinput/) - [RevTurbineTreatmentInteractionRequest](/revturbine-sdk/api/type-aliases/revturbinetreatmentinteractionrequest/) - [RevTurbineTrialContext](/revturbine-sdk/api/type-aliases/revturbinetrialcontext/) - [RevTurbineTriggerEvent](/revturbine-sdk/api/type-aliases/revturbinetriggerevent/) - [RevTurbineTriggerPayload](/revturbine-sdk/api/type-aliases/revturbinetriggerpayload/) - [RevTurbineUiPathActionTypes](/revturbine-sdk/api/type-aliases/revturbineuipathactiontypes/) - [RevTurbineUiPathResolver](/revturbine-sdk/api/type-aliases/revturbineuipathresolver/) - [RevTurbineUiPathResolverMap](/revturbine-sdk/api/type-aliases/revturbineuipathresolvermap/) - [RevTurbineUpdateInput](/revturbine-sdk/api/type-aliases/revturbineupdateinput/) - [RevTurbineUsageSnapshot](/revturbine-sdk/api/type-aliases/revturbineusagesnapshot/) - [SdkEventProperties](/revturbine-sdk/api/type-aliases/sdkeventproperties/) - [SdkMetadata](/revturbine-sdk/api/type-aliases/sdkmetadata/) - [SdkSessionOptions](/revturbine-sdk/api/type-aliases/sdksessionoptions/) - [SdkTraits](/revturbine-sdk/api/type-aliases/sdktraits/) - [SeatAssignmentErrorReason](/revturbine-sdk/api/type-aliases/seatassignmenterrorreason/) - [ServerActionHandler](/revturbine-sdk/api/type-aliases/serveractionhandler/) - [ServerActionMap](/revturbine-sdk/api/type-aliases/serveractionmap/) - [ServerEntitlementResult](/revturbine-sdk/api/type-aliases/serverentitlementresult/) - [ServerEvaluationHydrationPayload](/revturbine-sdk/api/type-aliases/serverevaluationhydrationpayload/) - [ServerPlacementDecision](/revturbine-sdk/api/type-aliases/serverplacementdecision/) - [ServerUserContext](/revturbine-sdk/api/type-aliases/serverusercontext/) - [SlotRouteParams](/revturbine-sdk/api/type-aliases/slotrouteparams/) - [SurfaceSlotCategory](/revturbine-sdk/api/type-aliases/surfaceslotcategory/) - [TelemetryConsent](/revturbine-sdk/api/type-aliases/telemetryconsent/) - [ToastComponentProps](/revturbine-sdk/api/type-aliases/toastcomponentprops/) - [TrackFn](/revturbine-sdk/api/type-aliases/trackfn/) - [TrackPurpose](/revturbine-sdk/api/type-aliases/trackpurpose/) - [UnvalidatedConfigArtifact](/revturbine-sdk/api/type-aliases/unvalidatedconfigartifact/) - [UsageBalances](/revturbine-sdk/api/type-aliases/usagebalances/) - [UsePlacementOptions](/revturbine-sdk/api/type-aliases/useplacementoptions/) - [UsePlacementResult](/revturbine-sdk/api/type-aliases/useplacementresult/) - [UserContextInput](/revturbine-sdk/api/type-aliases/usercontextinput/) ## Variables - [CHECKOUT\_STARTED\_CTA\_ACTION\_TYPES](/revturbine-sdk/api/variables/checkout_started_cta_action_types/) - [CONTROL\_PLANE\_EVENT\_SOURCE](/revturbine-sdk/api/variables/control_plane_event_source/) - [CONTROL\_PLANE\_SOURCE\_KEY](/revturbine-sdk/api/variables/control_plane_source_key/) - [DARK\_THEME](/revturbine-sdk/api/variables/dark_theme/) - [DEFAULT\_BRANDING](/revturbine-sdk/api/variables/default_branding/) - [DEFAULT\_HOSTED\_ENDPOINT](/revturbine-sdk/api/variables/default_hosted_endpoint/) - [DEFAULT\_SDK\_MODE](/revturbine-sdk/api/variables/default_sdk_mode/) - [DEFAULT\_THEME](/revturbine-sdk/api/variables/default_theme/) - [EMITTABLE\_PLATFORM\_EVENT\_NAMES](/revturbine-sdk/api/variables/emittable_platform_event_names/) - [FALLBACK\_ACCOUNT\_ID\_PREFIX](/revturbine-sdk/api/variables/fallback_account_id_prefix/) - [FIXED\_BANNER\_TEMPLATE\_IDS](/revturbine-sdk/api/variables/fixed_banner_template_ids/) - [FIXED\_SURFACE\_TEMPLATE\_IDS](/revturbine-sdk/api/variables/fixed_surface_template_ids/) - [GATED\_SURFACE\_TEMPLATE\_IDS](/revturbine-sdk/api/variables/gated_surface_template_ids/) - [GENERAL\_BANNER\_TEMPLATE\_IDS](/revturbine-sdk/api/variables/general_banner_template_ids/) - [GENERAL\_MODAL\_TEMPLATE\_IDS](/revturbine-sdk/api/variables/general_modal_template_ids/) - [GENERAL\_TOAST\_TEMPLATE\_IDS](/revturbine-sdk/api/variables/general_toast_template_ids/) - [INIT\_STATUS\_OK](/revturbine-sdk/api/variables/init_status_ok/) - [MESSAGE\_SURFACE\_TEMPLATE\_IDS](/revturbine-sdk/api/variables/message_surface_template_ids/) - [MessageSurfaceSlot](/revturbine-sdk/api/variables/messagesurfaceslot/) - [RECOGNIZED\_UPDATE\_KEYS](/revturbine-sdk/api/variables/recognized_update_keys/) - [RuntimeMode](/revturbine-sdk/api/variables/runtimemode/) - [SERVER\_TRAITS\_DOMAIN](/revturbine-sdk/api/variables/server_traits_domain/) - [SLOT\_ROUTE\_ID\_PLACEHOLDER](/revturbine-sdk/api/variables/slot_route_id_placeholder/) - [SLOT\_ROUTE\_MAX\_LENGTH](/revturbine-sdk/api/variables/slot_route_max_length/) ## Functions - [AccessDeniedPlaceholder](/revturbine-sdk/api/functions/accessdeniedplaceholder/) - [adaptExperimentVersionToBucketer](/revturbine-sdk/api/functions/adaptexperimentversiontobucketer/) - [BannerComponent](/revturbine-sdk/api/functions/bannercomponent/) - [BannerFrame](/revturbine-sdk/api/functions/bannerframe/) - [baseThemeForScheme](/revturbine-sdk/api/functions/basethemeforscheme/) - [bridgeUiPathResolversIntoRegistry](/revturbine-sdk/api/functions/bridgeuipathresolversintoregistry/) - [bucketSubject](/revturbine-sdk/api/functions/bucketsubject/) - [buildControlPlaneEvent](/revturbine-sdk/api/functions/buildcontrolplaneevent/) - [ButtonComponent](/revturbine-sdk/api/functions/buttoncomponent/) - [categorizeActionError](/revturbine-sdk/api/functions/categorizeactionerror/) - [clearPersistedTheme](/revturbine-sdk/api/functions/clearpersistedtheme/) - [collectAssignmentFactDeclarations](/revturbine-sdk/api/functions/collectassignmentfactdeclarations/) - [CompactCircularGauge](/revturbine-sdk/api/functions/compactcirculargauge/) - [composeEffectiveExperimentContext](/revturbine-sdk/api/functions/composeeffectiveexperimentcontext/) - [createAnalyticsProvider](/revturbine-sdk/api/functions/createanalyticsprovider/) - [createBasicExperimentProvider](/revturbine-sdk/api/functions/createbasicexperimentprovider/) - [createChainedPlacementRequest](/revturbine-sdk/api/functions/createchainedplacementrequest/) - [createCompositeExperimentProvider](/revturbine-sdk/api/functions/createcompositeexperimentprovider/) - [createCustomEndpointRuntimeConfig](/revturbine-sdk/api/functions/createcustomendpointruntimeconfig/) - [createEntitlementPlacementRequest](/revturbine-sdk/api/functions/createentitlementplacementrequest/) - [createLocalRuntimeConfig](/revturbine-sdk/api/functions/createlocalruntimeconfig/) - [createNativeExperimentAssignmentProvider](/revturbine-sdk/api/functions/createnativeexperimentassignmentprovider/) - [createPostHogAnalyticsProvider](/revturbine-sdk/api/functions/createposthoganalyticsprovider/) - [createPostHogIntegration](/revturbine-sdk/api/functions/createposthogintegration/) - [createRevTurbineApiClient](/revturbine-sdk/api/functions/createrevturbineapiclient/) - [createSemanticEvent](/revturbine-sdk/api/functions/createsemanticevent/) - [createServerRuntimeConfig](/revturbine-sdk/api/functions/createserverruntimeconfig/) - [createSlotPlacementRequest](/revturbine-sdk/api/functions/createslotplacementrequest/) - [createStrictLocalRuntimeConfig](/revturbine-sdk/api/functions/createstrictlocalruntimeconfig/) - [createTreatmentInteraction](/revturbine-sdk/api/functions/createtreatmentinteraction/) - [currentBrowserRoute](/revturbine-sdk/api/functions/currentbrowserroute/) - [defineUiPathResolvers](/revturbine-sdk/api/functions/defineuipathresolvers/) - [dispatchCtaClick](/revturbine-sdk/api/functions/dispatchctaclick/) - [EmailPreviewComponent](/revturbine-sdk/api/functions/emailpreviewcomponent/) - [EngagementArea](/revturbine-sdk/api/functions/engagementarea/) - [entitlementResultDenies](/revturbine-sdk/api/functions/entitlementresultdenies/) - [fallbackAccountId](/revturbine-sdk/api/functions/fallbackaccountid/) - [FixedSurfaceSlot](/revturbine-sdk/api/functions/fixedsurfaceslot/) - [formatCurrencyMinorUnits](/revturbine-sdk/api/functions/formatcurrencyminorunits/) - [Gate](/revturbine-sdk/api/functions/gate/) - [getDefaultCtaResolverRegistry](/revturbine-sdk/api/functions/getdefaultctaresolverregistry/) - [getDefaultRegistry](/revturbine-sdk/api/functions/getdefaultregistry/) - [InitFailureDiagnostic](/revturbine-sdk/api/functions/initfailurediagnostic/) - [initRevTurbine](/revturbine-sdk/api/functions/initrevturbine/) - [initStatusForError](/revturbine-sdk/api/functions/initstatusforerror/) - [InlineCardPlacement](/revturbine-sdk/api/functions/inlinecardplacement/) - [InlineEmbedComponent](/revturbine-sdk/api/functions/inlineembedcomponent/) - [isExperimentAssignmentFactDeclaration](/revturbine-sdk/api/functions/isexperimentassignmentfactdeclaration/) - [isFallbackAccountId](/revturbine-sdk/api/functions/isfallbackaccountid/) - [isIdentifierLikeSegment](/revturbine-sdk/api/functions/isidentifierlikesegment/) - [loadTheme](/revturbine-sdk/api/functions/loadtheme/) - [mergeTheme](/revturbine-sdk/api/functions/mergetheme/) - [ModalComponent](/revturbine-sdk/api/functions/modalcomponent/) - [ModalFrame](/revturbine-sdk/api/functions/modalframe/) - [normalizeConfigArtifactOrThrow](/revturbine-sdk/api/functions/normalizeconfigartifactorthrow/) - [normalizeSlotRoute](/revturbine-sdk/api/functions/normalizeslotroute/) - [parsePromotion](/revturbine-sdk/api/functions/parsepromotion/) - [parseUiPath](/revturbine-sdk/api/functions/parseuipath/) - [Placement](/revturbine-sdk/api/functions/placement/) - [PlacementDecisionInspector](/revturbine-sdk/api/functions/placementdecisioninspector/) - [PlacementRenderer](/revturbine-sdk/api/functions/placementrenderer/) - [PushPreviewComponent](/revturbine-sdk/api/functions/pushpreviewcomponent/) - [QuotaMeterFrame](/revturbine-sdk/api/functions/quotameterframe/) - [registerBuiltinSlotTypes](/revturbine-sdk/api/functions/registerbuiltinslottypes/) - [registerBuiltinSnoozeResolver](/revturbine-sdk/api/functions/registerbuiltinsnoozeresolver/) - [registerCtaResolver](/revturbine-sdk/api/functions/registerctaresolver/) - [registerServerActionResolvers](/revturbine-sdk/api/functions/registerserveractionresolvers/) - [remediationFor](/revturbine-sdk/api/functions/remediationfor/) - [resetDefaultCtaResolverRegistry](/revturbine-sdk/api/functions/resetdefaultctaresolverregistry/) - [resetDefaultRegistry](/revturbine-sdk/api/functions/resetdefaultregistry/) - [resolveBranding](/revturbine-sdk/api/functions/resolvebranding/) - [resolveContent](/revturbine-sdk/api/functions/resolvecontent/) - [resolveLocalPlaybook](/revturbine-sdk/api/functions/resolvelocalplaybook/) - [resolvePersistentStorage](/revturbine-sdk/api/functions/resolvepersistentstorage/) - [resolveSessionStorage](/revturbine-sdk/api/functions/resolvesessionstorage/) - [resolveTokens](/revturbine-sdk/api/functions/resolvetokens/) - [RevTurbineProvider](/revturbine-sdk/api/functions/revturbineprovider/) - [RevTurbineRoute](/revturbine-sdk/api/functions/revturbineroute/) - [RevTurbineThemeProvider](/revturbine-sdk/api/functions/revturbinethemeprovider/) - [routePatternFromParams](/revturbine-sdk/api/functions/routepatternfromparams/) - [Slot](/revturbine-sdk/api/functions/slot/) - [SmsPreviewComponent](/revturbine-sdk/api/functions/smspreviewcomponent/) - [TelemetryScope](/revturbine-sdk/api/functions/telemetryscope/) - [Track](/revturbine-sdk/api/functions/track/) - [TrackOnView](/revturbine-sdk/api/functions/trackonview/) - [unregisterCtaResolver](/revturbine-sdk/api/functions/unregisterctaresolver/) - [useAddons](/revturbine-sdk/api/functions/useaddons/) - [useCan](/revturbine-sdk/api/functions/usecan/) - [useEntitlement](/revturbine-sdk/api/functions/useentitlement/) - [useGatedAction](/revturbine-sdk/api/functions/usegatedaction/) - [usePlacement](/revturbine-sdk/api/functions/useplacement/) - [usePlacementPersonalization](/revturbine-sdk/api/functions/useplacementpersonalization/) - [usePlans](/revturbine-sdk/api/functions/useplans/) - [useResolvedColorScheme](/revturbine-sdk/api/functions/useresolvedcolorscheme/) - [useRevTurbineTheme](/revturbine-sdk/api/functions/userevturbinetheme/) - [useRevTurbineThemeProviderPresent](/revturbine-sdk/api/functions/userevturbinethemeproviderpresent/) - [UserProfile](/revturbine-sdk/api/functions/userprofile/) - [useSlotRoute](/revturbine-sdk/api/functions/useslotroute/) - [useSurfaceSlot](/revturbine-sdk/api/functions/usesurfaceslot/) - [useTelemetryProps](/revturbine-sdk/api/functions/usetelemetryprops/) - [useTelemetryScope](/revturbine-sdk/api/functions/usetelemetryscope/) - [useTrack](/revturbine-sdk/api/functions/usetrack/) - [useTrackedAction](/revturbine-sdk/api/functions/usetrackedaction/) ## References ### ~~BannerSlot~~ Renames and re-exports [BannerComponent](/revturbine-sdk/api/functions/bannercomponent/) *** ### ~~BannerSlotProps~~ Renames and re-exports [BannerComponentProps](/revturbine-sdk/api/type-aliases/bannercomponentprops/) *** ### ~~ButtonSlot~~ Renames and re-exports [ButtonComponent](/revturbine-sdk/api/functions/buttoncomponent/) *** ### ~~ButtonSlotProps~~ Renames and re-exports [ButtonComponentProps](/revturbine-sdk/api/type-aliases/buttoncomponentprops/) *** ### ~~CliSlotProps~~ Renames and re-exports [CliComponentProps](/revturbine-sdk/api/type-aliases/clicomponentprops/) *** ### ~~CreditBalanceSlotProps~~ Renames and re-exports [CreditBalanceComponentProps](/revturbine-sdk/api/type-aliases/creditbalancecomponentprops/) *** ### ~~EmailPreviewSlot~~ Renames and re-exports [EmailPreviewComponent](/revturbine-sdk/api/functions/emailpreviewcomponent/) *** ### ~~FullPageSlotProps~~ Renames and re-exports [FullPageComponentProps](/revturbine-sdk/api/type-aliases/fullpagecomponentprops/) *** ### ~~InlineEmbedSlot~~ Renames and re-exports [InlineEmbedComponent](/revturbine-sdk/api/functions/inlineembedcomponent/) *** ### ~~InlineEmbedSlotProps~~ Renames and re-exports [InlineEmbedComponentProps](/revturbine-sdk/api/type-aliases/inlineembedcomponentprops/) *** ### ~~InPageSlotProps~~ Renames and re-exports [InPageComponentProps](/revturbine-sdk/api/type-aliases/inpagecomponentprops/) *** ### ~~ModalSlot~~ Renames and re-exports [ModalComponent](/revturbine-sdk/api/functions/modalcomponent/) *** ### ~~ModalSlotProps~~ Renames and re-exports [ModalComponentProps](/revturbine-sdk/api/type-aliases/modalcomponentprops/) *** ### ~~PushPreviewSlot~~ Renames and re-exports [PushPreviewComponent](/revturbine-sdk/api/functions/pushpreviewcomponent/) *** ### ~~QuotaMeterSlotProps~~ Renames and re-exports [QuotaMeterComponentProps](/revturbine-sdk/api/type-aliases/quotametercomponentprops/) *** ### ~~RTSlot~~ Renames and re-exports [Slot](/revturbine-sdk/api/functions/slot/) *** ### ~~SmsPreviewSlot~~ Renames and re-exports [SmsPreviewComponent](/revturbine-sdk/api/functions/smspreviewcomponent/) *** ### ~~ToastSlotProps~~ Renames and re-exports [ToastComponentProps](/revturbine-sdk/api/type-aliases/toastcomponentprops/) # ActionErrorCategory > **ActionErrorCategory** = `"aborted"` \| `"timeout"` \| `"network"` \| `"validation"` \| `"permission"` \| `"unknown"` A non-sensitive, normalized error bucket for a failed action (plan 144 TASK-14). Deliberately coarse — it never carries the raw error message, which could contain user input or PII. # AnalyticsEventHandler > **AnalyticsEventHandler** = (`eventName`, `properties`) => `void` Callback invoked for every SDK event. ## Parameters ### eventName `string` Normalised event name (e.g. `'placement_interaction'`). ### properties [`AnalyticsEventProperties`](/revturbine-sdk/api/type-aliases/analyticseventproperties/) Flat key-value properties derived from the event envelope. ## Returns `void` # AnalyticsEventProperties > **AnalyticsEventProperties** = `Record`\<`string`, `unknown`\> Flat properties object passed to the analytics handler after transforming a raw RevTurbineEventEnvelope. # AnalyticsEventTransformer > **AnalyticsEventTransformer** = (`eventName`, `properties`) => \{ `eventName`: `string`; `properties`: [`AnalyticsEventProperties`](/revturbine-sdk/api/type-aliases/analyticseventproperties/); \} \| `null` Optional transformer applied before the handler. Return `null` to drop the event silently. ## Parameters ### eventName `string` ### properties [`AnalyticsEventProperties`](/revturbine-sdk/api/type-aliases/analyticseventproperties/) ## Returns \{ `eventName`: `string`; `properties`: [`AnalyticsEventProperties`](/revturbine-sdk/api/type-aliases/analyticseventproperties/); \} \| `null` # AssignmentFactUnit > **AssignmentFactUnit** = `NonNullable`\<`Experiment`\[`"assignment_unit"`\]\> One supported canonical experiment assignment unit (war-games spec §9.1). # BannerComponentProps > **BannerComponentProps** = [`PlacementSlotProps`](/revturbine-sdk/api/interfaces/placementslotprops/) & `object` Props for [BannerSlot](/revturbine-sdk/api/functions/bannercomponent/). Extends [PlacementSlotProps](/revturbine-sdk/api/interfaces/placementslotprops/) with banner-specific options. ## Type Declaration ### position? > `optional` **position?**: `"top"` \| `"bottom"` Banner position — sticks to the top or bottom of the viewport. # BrandingSource > **BrandingSource** = `"explicit"` \| `"legacy-config"` \| `"branding-api"` \| `"default"` The rung of the branding ladder a resolved value came from. Returned alongside the resolved branding so callers (and tests) can assert provenance and surface the development deprecation warning for the legacy rung. # BrowserRuntimeOptions > **BrowserRuntimeOptions** = `BrowserRuntimeOptionsBase` & \{ `exportedConfig?`: [`ConfigArtifact`](/revturbine-sdk/api/type-aliases/configartifact/); `playbook`: [`ConfigArtifact`](/revturbine-sdk/api/type-aliases/configartifact/); \} \| \{ `exportedConfig`: [`ConfigArtifact`](/revturbine-sdk/api/type-aliases/configartifact/); `playbook?`: [`ConfigArtifact`](/revturbine-sdk/api/type-aliases/configartifact/); \} Options for BrowserRuntime. Exactly one of `playbook` (canonical) or `exportedConfig` (deprecated) is required — the union keeps that a compile-time requirement, exactly as the single required `exportedConfig` property did before the rename (BL-0156). # BuiltinDimensionsErrorReason > **BuiltinDimensionsErrorReason** = `"invalid_request"` \| `"unauthorized"` \| `"forbidden"` \| `"request_failed"` Why a built-in dimension read was refused, derived from the HTTP status of `GET /api/sdk/user-contexts/{userId}/builtin-dimensions`: - `invalid_request` (400) — e.g. a blank user id. - `unauthorized` (401) — missing or invalid credential. - `forbidden` (403) — the credential is not a **server** key, the request looked browser-originated, or a tenant header named another tenant. - `request_failed` — any other non-2xx status. # ButtonComponentProps > **ButtonComponentProps** = [`PlacementSlotProps`](/revturbine-sdk/api/interfaces/placementslotprops/) & `object` Props for [ButtonSlot](/revturbine-sdk/api/functions/buttoncomponent/). Extends [PlacementSlotProps](/revturbine-sdk/api/interfaces/placementslotprops/) with button styling options. ## Type Declaration ### buttonStyle? > `optional` **buttonStyle?**: `"primary"` \| `"secondary"` \| `"accent"` Visual style variant. Default `'primary'`. # ChangeListener > **ChangeListener** = () => `void` Callback used to subscribe to state changes in controllers. ## Returns `void` # CliComponentProps > **CliComponentProps** = [`PlacementSlotProps`](/revturbine-sdk/api/interfaces/placementslotprops/) Props for CliSlot. Uses the standard [PlacementSlotProps](/revturbine-sdk/api/interfaces/placementslotprops/) without extensions. # ConfigArtifact > **ConfigArtifact** = `Playbook` \| `RevTurbineConfig` A canonical Playbook or the deprecated RevTurbineConfig wire shape. # CreditBalanceComponentProps > **CreditBalanceComponentProps** = [`PlacementSlotProps`](/revturbine-sdk/api/interfaces/placementslotprops/) & `object` Props for CreditBalanceSlot. Extends [PlacementSlotProps](/revturbine-sdk/api/interfaces/placementslotprops/) with balance display options. ## Type Declaration ### displayStyle? > `optional` **displayStyle?**: `"numeric_balance"` \| `"balance_bar"` \| `"balance_burn_rate"` Visual display variant. - `numeric_balance` — large number (default) - `balance_bar` — horizontal progress bar - `balance_burn_rate` — bar with burn rate label # CtaResolver > **CtaResolver** = (`uiPath`, `context`) => `void` A resolver bound to a CTA action type via `registerCtaResolver`. When a placement's CTA is clicked and a resolver is registered for the parsed `uiPath.type`, the renderer invokes it instead of the generic `onCtaClick` callback — letting customers handle tenant-defined custom actions (e.g. opening an integration flow) with their own logic. ## Parameters ### uiPath [`PlacementUiPath`](/revturbine-sdk/api/interfaces/placementuipath/) ### context [`CtaResolverContext`](/revturbine-sdk/api/interfaces/ctaresolvercontext/) ## Returns `void` # DimensionWriteErrorReason > **DimensionWriteErrorReason** = `"invalid_request"` \| `"unauthorized"` \| `"forbidden"` \| `"unknown_dimension_value"` \| `"request_failed"` Why a built-in dimension write ([RevTurbineServer.setBuiltinDimensions](/revturbine-sdk/api/classes/revturbineserver/#setbuiltindimensions)) was refused, derived from the HTTP status of `POST /api/sdk/user-contexts`: - `invalid_request` (400) — malformed body: an empty user id, an empty dimensions map, `seat_type` (use `assignSeatType`) or an unknown key. - `unauthorized` (401) — missing or invalid credential. - `forbidden` (403) — the credential is not a **server** key. - `unknown_dimension_value` (422) — a value is outside its dimension's vocabulary. - `request_failed` — any other non-2xx status. # EmittablePlatformEventName > **EmittablePlatformEventName** = `Exclude`\<`PlatformEventName`, `ControlPlaneEventName`\> Every platform event the typed emit surface accepts: the full taxonomy MINUS the control-plane surface (structural exclusion — the CP has its own typed path with source classification, and a customer emitting `playbook_version_deployed` would be forging RevTurbine's own product telemetry). Includes the R-2 billing vocabulary: billing facts consumed from Stripe must be equally emittable first-party (plan 228 R-1). # EntitlementStatus > **EntitlementStatus** = `SchemaEntitlementStatus` Canonical entitlement check status. Re-exported from `@revt-eng/schema`. # ExactInitOptions > **ExactInitOptions**\<`T`\> = `{ [K in keyof T]: K extends RevTurbineInitOptionKey ? T[K] : never }` Exact-check for init options (plan 233 REQ-2b). A key the options do not declare becomes `never`, which turns a typo or a stale option into a compile error instead of a value the SDK silently ignores. The motivating shape is the same one `Exact` was introduced for in plan 191: TypeScript's excess-property check applies only to fresh object literals, so options assembled in a variable — or in an un-annotated `useMemo`, which is exactly how React integrations build them — carried unknown keys with no diagnostic at all. Scope limit worth stating: this cannot catch a wrong VALUE. The escalated `tenant_id` defect was never reachable from here — that one is caught by init-time validation (REQ-1/REQ-2) and the reachable init status (REQ-3). It is *not* true that a Playbook is beyond compile-time reach. An earlier version of this note claimed a served artifact is `Record` “by necessity”; that was wrong. [UnvalidatedConfigArtifact](/revturbine-sdk/api/type-aliases/unvalidatedconfigartifact/) structurally requires the body arrays, so a truncated Playbook fails to compile at the `localRuntime` boundary. What genuinely cannot be checked is the artifact's *contents* — whether a plan handle exists, whether a rule is satisfiable. ## Type Parameters ### T `T` # ExportedConfigProvider > **ExportedConfigProvider** = [`RevTurbineConfigProvider`](/revturbine-sdk/api/interfaces/revturbineconfigprovider/) :::caution[Deprecated] Renamed to [RevTurbineConfigProvider](/revturbine-sdk/api/interfaces/revturbineconfigprovider/) (plan 104). Kept as a back-compat alias so existing integrations keep compiling. Removed in `0.13.0` alongside the other `ExportedConfig`-spelled aliases (BL-0156). Use `RevTurbineConfigProvider`. ::: # FullPageComponentProps > **FullPageComponentProps** = [`PlacementSlotProps`](/revturbine-sdk/api/interfaces/placementslotprops/) & `object` Props for FullPageSlot. Extends [PlacementSlotProps](/revturbine-sdk/api/interfaces/placementslotprops/) with page template options. ## Type Declaration ### pageTemplate? > `optional` **pageTemplate?**: `"plan_cards"` \| `"feature_comparison"` \| `"upgrade_comparison"` \| `"custom"` Page template layout. Currently informational; rendering is uniform. # IdentifyContextInput > **IdentifyContextInput** = `Partial`\<`Omit`\<[`UserContextInput`](/revturbine-sdk/api/type-aliases/usercontextinput/), `ServerAssignedUserContextKey` \| `IdentifyUnmergedUserContextKey`\>\> & `object` Input accepted by [RevTurbineCustomerSdk.identify](/revturbine-sdk/api/classes/revturbinecustomersdk/#identify) for the canonical user-context shape (plan 191). `plan_handle` is THE plan matching identity — the plan's `unique_handle`; the `plan` object is display metadata (name/price/period) and never participates in matching. Server-assigned fields (`seat_type_handle`, `activity_score`, `activity_score_computed_at`, `builtin_dimensions`) are not accepted here (plan 279, D-36, D-46; BL-0352, BL-0381); the tenant's backend or RevTurbine's enrichment writes them. Since 0.12.0 the type also omits the user-context fields `identify()` never merged (it dropped them with a warning): trial, billing and tier state, instances, experiments and the derived-context bookkeeping. Set those through [RevTurbineCustomerSdk.update](/revturbine-sdk/api/classes/revturbinecustomersdk/#update) / `setUserContext`, or let client-context enrichment deliver them. ## Type Declaration ### plan\_handle? > `optional` **plan\_handle?**: `string` THE plan matching identity — the plan's `unique_handle` (e.g. `'pro'`). # InlineEmbedComponentProps > **InlineEmbedComponentProps** = [`PlacementSlotProps`](/revturbine-sdk/api/interfaces/placementslotprops/) Props for [InlineEmbedSlot](/revturbine-sdk/api/functions/inlineembedcomponent/). # InPageComponentProps > **InPageComponentProps** = [`InlineEmbedComponentProps`](/revturbine-sdk/api/type-aliases/inlineembedcomponentprops/) Explicit in-page surface renderer. This is the canonical SDK component for the `in_page` surface type. It wraps `InlineEmbedSlot` for backwards compatibility. # LegacyConfigTargetDefaults > **LegacyConfigTargetDefaults** = [`ConfigTargetDefaults`](/revturbine-sdk/api/interfaces/configtargetdefaults/) :::caution[Deprecated] Renamed to [ConfigTargetDefaults](/revturbine-sdk/api/interfaces/configtargetdefaults/) in plan 233 — these values are no longer "legacy defaults" applied only to pre-stamping artifacts; they are the authoritative target for every artifact shape. Kept as an alias so the already-shipped export keeps resolving. ::: # MessageTriggerType > **MessageTriggerType** = `"on_mount"` \| `"on_event"` \| `"on_interval"` Trigger type that controls when the message placement evaluates and renders. - `'on_mount'` — Evaluate immediately when the component mounts. - `'on_event'` — Evaluate when `triggerEvent` is called externally. - `'on_interval'` — Re-evaluate on a polling interval. # ModalComponentProps > **ModalComponentProps** = [`PlacementSlotProps`](/revturbine-sdk/api/interfaces/placementslotprops/) & `object` Props for [ModalSlot](/revturbine-sdk/api/functions/modalcomponent/). Extends [PlacementSlotProps](/revturbine-sdk/api/interfaces/placementslotprops/) with modal-specific options. ## Type Declaration ### modalType? > `optional` **modalType?**: `"optional"` \| `"blocking"` Modal dismissibility behavior. - `optional` — user can dismiss via ESC, close button, or clicking outside - `blocking` — user must interact with a CTA (no dismiss controls) # ModalLayout > **ModalLayout** = `"centered"` \| `"two_column"` Modal layout variants: - `centered` — single column, centered content (default, existing behavior) - `two_column` — left content + right visual panel (paywall/trial style) # NativeExperimentAssignmentUnit > **NativeExperimentAssignmentUnit** = `NonNullable`\<`Experiment`\[`"assignment_unit"`\]\> One supported canonical experiment assignment unit. # PlacementPromotion > **PlacementPromotion** = `RuntimePromotionSnapshot` Promotion attached to a placement payload. Re-exported from the schema's RuntimePromotionSnapshot — lightweight promotion snapshot for placement rendering contexts. # PlacementProps > **PlacementProps** = `object` Props for the [Placement](/revturbine-sdk/api/functions/placement/) render-prop component. ## Properties ### autoLoad? > `optional` **autoLoad?**: `boolean` Auto-load on mount. *** ### children > **children**: (`args`) => `React.ReactNode` Render function receiving the placement state. #### Parameters ##### args `PlacementRenderArgs` #### Returns `React.ReactNode` *** ### contextMode? > `optional` **contextMode?**: `RevTurbineContextMode` Context resolution mode. *** ### fallback? > `optional` **fallback?**: `React.ReactNode` Fallback UI when the placement is not visible. *** ### overrides? > `optional` **overrides?**: `RevTurbinePlacementDecisionOverrides` Decision overrides for testing. *** ### placement > **placement**: [`RevTurbinePlacementConfig`](/revturbine-sdk/api/interfaces/revturbineplacementconfig/) Placement configuration. *** ### traits? > `optional` **traits?**: `Record`\<`string`, `string` \| `number` \| `boolean`\> Custom traits. *** ### ttlMs? > `optional` **ttlMs?**: `number` Cache TTL in milliseconds. *** ### userId? > `optional` **userId?**: `string` Target user ID. # PlacementUiPathActionType > **PlacementUiPathActionType** = `RevTurbineConfigUiPathActionType` \| `string` & `object` CTA action types recognized by the SDK. The built-in action types come from the schema's `RevTurbineConfigUiPathActionType` (the single source of truth). The `(string & {})` member keeps autocomplete for the built-ins while also admitting **tenant-defined custom action names** — a placement authored with a `custom` (or any unmapped) CTA action flows through the engine as `{ type: , ...config }`, and the SDK preserves that name so a [CtaResolver](/revturbine-sdk/api/type-aliases/ctaresolver/) can be registered against it via `registerCtaResolver`. # PlaybookProvider > **PlaybookProvider** = [`RevTurbineConfigProvider`](/revturbine-sdk/api/interfaces/revturbineconfigprovider/) The Playbook-named counterpart of the deprecated [ExportedConfigProvider](/revturbine-sdk/api/type-aliases/exportedconfigprovider/) alias (BL-0156). Identical to [RevTurbineConfigProvider](/revturbine-sdk/api/interfaces/revturbineconfigprovider/), which remains the primary name. This exists so code migrating off `ExportedConfigProvider` has a Playbook-spelled name to land on rather than only the `RevTurbineConfig` one. # QuotaMeterComponentProps > **QuotaMeterComponentProps** = [`PlacementSlotProps`](/revturbine-sdk/api/interfaces/placementslotprops/) & `object` Props for QuotaMeterSlot. Extends [PlacementSlotProps](/revturbine-sdk/api/interfaces/placementslotprops/) with display style options. ## Type Declaration ### displayStyle? > `optional` **displayStyle?**: `"progress_bar"` \| `"circular_gauge"` \| `"numeric_counter"` Visual display variant. - `progress_bar` — horizontal bar (default) - `circular_gauge` — SVG circle - `numeric_counter` — large number display # RecognizedIdentifyKey > **RecognizedIdentifyKey** = *typeof* `RECOGNIZED_IDENTIFY_KEYS`\[`number`\] The top-level [IdentifyContextInput](/revturbine-sdk/api/type-aliases/identifycontextinput/) keys [RevTurbineCustomerSdk.identify](/revturbine-sdk/api/classes/revturbinecustomersdk/#identify) merges into the held context (the entries of its recognized-key list). Exported for type-level exhaustiveness checks; it has no runtime value. # RecognizedUpdateKey > **RecognizedUpdateKey** = *typeof* [`RECOGNIZED_UPDATE_KEYS`](/revturbine-sdk/api/variables/recognized_update_keys/)\[`number`\] The top-level [RevTurbineUpdateInput](/revturbine-sdk/api/type-aliases/revturbineupdateinput/) keys [RevTurbineCustomerSdk.update](/revturbine-sdk/api/classes/revturbinecustomersdk/#update) patches (the entries of [RECOGNIZED\_UPDATE\_KEYS](/revturbine-sdk/api/variables/recognized_update_keys/)). Exported for type-level exhaustiveness checks; it has no runtime value. # RegisterPlacementSlotTypeOptions > **RegisterPlacementSlotTypeOptions**\<`P`\> = `Omit`\<[`PlacementSlotType`](/revturbine-sdk/api/interfaces/placementslottype/)\<`P`\>, `"componentType"` \| `"accepts"` \| `"defaultProps"` \| `"priority"` \| `"renderedFields"`\> & `object` Options for registering a custom placement type. Identical to PlacementSlotType but all fields are required except defaultProps, accepts, and priority. ## Type Declaration ### accepts? > `optional` **accepts?**: (`output`) => `boolean` #### Parameters ##### output `PlacementOutput` #### Returns `boolean` ### componentType? > `optional` **componentType?**: `RevTurbineComponentType` Canonical component type handled by this renderer. ### defaultProps? > `optional` **defaultProps?**: `Partial`\<`P`\> ### priority? > `optional` **priority?**: `number` Resolution priority (default `0`). Higher values are evaluated first. ### renderedFields? > `optional` **renderedFields?**: readonly `string`[] Machine-readable fields rendered by this component. ### ~~surfaceType?~~ > `optional` **surfaceType?**: `RevTurbineComponentType` :::caution[Deprecated] Use `componentType`. ::: ## Type Parameters ### P `P` *extends* [`PlacementSlotProps`](/revturbine-sdk/api/interfaces/placementslotprops/) = [`PlacementSlotProps`](/revturbine-sdk/api/interfaces/placementslotprops/) # ResolvedContent > **ResolvedContent** = [`PlacementContentFields`](/revturbine-sdk/api/interfaces/placementcontentfields/) & `Record`\<`string`, `unknown`\> Resolved content with personalization tokens expanded. # RevTurbineClientSessionProvider > **RevTurbineClientSessionProvider** = () => `string` \| `Promise`\<`string`\> Mints a short-lived client-session token for the signed-in user (plan 191 Q-6). Called by the SDK on first need, after `identify()`, and again when a token is rejected as expired. Return the raw `rt_client_…` token your backend minted from `POST /api/sdk/client-sessions`. ## Returns `string` \| `Promise`\<`string`\> # RevTurbineColorScheme > **RevTurbineColorScheme** = `"light"` \| `"dark"` \| `"system"` The colour scheme a theme resolves against. `'system'` follows the OS/browser `prefers-color-scheme` and keeps following it as the user changes it. # RevTurbineConfigProviderInput > **RevTurbineConfigProviderInput** = [`RevTurbineConfigProvider`](/revturbine-sdk/api/interfaces/revturbineconfigprovider/) & \{ `getPlaybook`: [`ConfigArtifact`](/revturbine-sdk/api/type-aliases/configartifact/) \| `undefined`; \} \| \{ `getExportedConfig`: [`ConfigArtifact`](/revturbine-sdk/api/type-aliases/configartifact/) \| `undefined`; \} A [RevTurbineConfigProvider](/revturbine-sdk/api/interfaces/revturbineconfigprovider/) that supplies at least one of the two Playbook accessors. This is the shape the SDK actually accepts — the bare interface leaves both optional only so an existing `implements` clause keeps compiling. # RevTurbineGateResult > **RevTurbineGateResult**\<`T`\> = \{ `entitlement`: `EntitlementResult`; `ran`: `true`; `result`: `T`; \} \| \{ `entitlement`: `EntitlementResult`; `ran`: `false`; \} Outcome of [RevTurbineCustomerSdk.gate](/revturbine-sdk/api/classes/revturbinecustomersdk/#gate) — the advertised `gate(action, fn)` verb. When the action was permitted, `ran` is `true` and `result` holds the callback's return value; otherwise `ran` is `false`, the callback did not run, and the caller should surface the `entitlement` (e.g. render an `` paywall). ## Type Parameters ### T `T` # RevTurbineImpressionMetadata > **RevTurbineImpressionMetadata** = `Record`\<`string`, `unknown`\> Arbitrary key/value metadata persisted with an impression record. Free-form by design — consumers attach whatever context is useful for downstream analytics (CTA type, variant id, surface slot id, experiment arm, etc.). The shape is intentionally untyped because it's a passthrough into the scaffold's ImpressionRecord.metadata field, which is the same type (`Record`) on the persistence side. Defining the alias here gives the SDK type- safety scan one place to suppress instead of three identical parameter sites. # RevTurbineInitBaseOptions > **RevTurbineInitBaseOptions** = `Omit`\<`RevTurbineInitOptions`, `"runtimeMode"` \| `"endpointOverrides"` \| `"localRuntime"`\> Base init options shared by all runtime mode helper builders. # RevTurbineInitInputOptions > **RevTurbineInitInputOptions** = `RevTurbineInitWithProviderOptions` \| [`RevTurbineLocalOnlyMinimalInitOptions`](/revturbine-sdk/api/type-aliases/revturbinelocalonlyminimalinitoptions/) & `RevTurbineProviderOptionAugmentations` Public SDK initialization input. Accepts either full options for any runtime mode, or local-only minimal options when `localRuntime.playbook` (or the deprecated `exportedConfig` alias) is provided. # RevTurbineInitOptionsStrict > **RevTurbineInitOptionsStrict** = `RevTurbineBaseInitWithoutRuntimeSpecifics` & `object` \| `RevTurbineBaseInitWithoutRuntimeSpecifics` & `object` \| `RevTurbineBaseInitWithoutRuntimeSpecifics` & `object` Strict initialization mode for compile-time safety: when using `local_only` runtime with a Playbook (`localRuntime.playbook`, or the legacy `localRuntime.exportedConfig`), `uiPathResolvers` is required. # RevTurbineInitPhase > **RevTurbineInitPhase** = `"construct"` \| `"identify"` \| `"theme"` \| `"placements"` \| `"bootstrap"` Where initialization was when it failed. Useful because the phases fail for different reasons: `construct` is almost always malformed options, `theme` a branding/network problem, `bootstrap` a placement that does not resolve. # RevTurbineLocalOnlyMinimalInitOptions > **RevTurbineLocalOnlyMinimalInitOptions** = `Omit`\<`RevTurbineInitOptions`, `"tenantId"` \| `"apiKey"` \| `"endpoint"` \| `"mode"` \| `"runtimeMode"` \| `"localRuntime"`\> & `object` Minimal initialization options for local-only mode. When `localRuntime.playbook` (or the deprecated `exportedConfig` alias) is provided, core transport options can be omitted and the SDK will inject safe local defaults. `playbook` is the canonical key; the runtime resolves both through [resolveLocalPlaybook](/revturbine-sdk/api/functions/resolvelocalplaybook/). ## Type Declaration ### apiKey? > `optional` **apiKey?**: `string` ### endpoint? > `optional` **endpoint?**: `string` ### localRuntime > **localRuntime**: `RevTurbineLocalRuntimeOptions` & \{ `playbook`: [`ConfigArtifact`](/revturbine-sdk/api/type-aliases/configartifact/) \| [`UnvalidatedConfigArtifact`](/revturbine-sdk/api/type-aliases/unvalidatedconfigartifact/); \} \| \{ `exportedConfig`: [`ConfigArtifact`](/revturbine-sdk/api/type-aliases/configartifact/) \| [`UnvalidatedConfigArtifact`](/revturbine-sdk/api/type-aliases/unvalidatedconfigartifact/); \} ### mode? > `optional` **mode?**: [`RevTurbineSdkMode`](/revturbine-sdk/api/type-aliases/revturbinesdkmode/) ### runtimeMode? > `optional` **runtimeMode?**: `"local_only"` ### tenantId? > `optional` **tenantId?**: `string` # RevTurbinePlaybookLoadState > **RevTurbinePlaybookLoadState** = `"ready"` \| `"loading"` \| `"unavailable"` Where the Playbook config load stands (BL-0177). `config_unavailable` used to be a single undifferentiated outcome, so a slot that lost the race against a cold config fetch could not tell "wait, it is coming" from "nothing is coming" and rendered its fallback permanently. This is the distinction: `loading` makes a `config_unavailable` decision retriable, `unavailable` makes it terminal for now. # RevTurbineRequiredUiPathResolvers > **RevTurbineRequiredUiPathResolvers**\<`TUiPaths`\> = `{ [ActionType in RevTurbineUiPathActionTypes]: RevTurbineUiPathResolver }` Resolver map that requires handlers for every `action_type` present in `TUiPaths`. ## Type Parameters ### TUiPaths `TUiPaths` *extends* readonly `unknown`[] # RevTurbineRuntimeMode > **RevTurbineRuntimeMode** = *typeof* [`RuntimeMode`](/revturbine-sdk/api/variables/runtimemode/)\[keyof *typeof* [`RuntimeMode`](/revturbine-sdk/api/variables/runtimemode/)\] The set of valid runtime modes (plan 144), derived from [RuntimeMode](/revturbine-sdk/api/variables/runtimemode/). # RevTurbineSdkMode > **RevTurbineSdkMode** = `"snippet"` \| `"react"` \| `"iframe"` SDK integration mode. - `snippet` — lightweight script tag embed - `react` — React component integration - `iframe` — sandboxed iframe embed # RevTurbineSegmentPredicateEvaluation > **RevTurbineSegmentPredicateEvaluation** = `PredicateEvaluationResult` Predicate-level evaluation detail for segment matching diagnostics. # RevTurbineThemeInput > **RevTurbineThemeInput** = `object` Partial theme for API responses and user uploads. All values are optional — the SDK deep-merges with defaults. # RevTurbineTreatmentInteractionRequest > **RevTurbineTreatmentInteractionRequest** = `RevTurbineTreatmentInteractionInput` The full input [RevTurbineCustomerSdk.trackTreatmentInteraction](/revturbine-sdk/api/classes/revturbinecustomersdk/#tracktreatmentinteraction) accepts — a plain alias of core's RevTurbineTreatmentInteractionInput (plan 282 TASK-9 follow-up, BL-0463). Through scaffold 0.1.382 this file defined its own `RevTurbineTreatmentInteractionDecisionContext` (`decisionId`, `segmentHandles`, `segmentIds`) and intersected it onto core's input type, because core did not carry those fields yet. Scaffold 0.1.383 (#439) moved that same shape — TSDoc included — onto RevTurbineTreatmentInteractionInput itself, so the intersection had nothing left to add. This alias exists only so the exported name stays `RevTurbineTreatmentInteractionRequest` for callers; the type it names is now exactly core's. # RevTurbineTrialContext > **RevTurbineTrialContext** = `UserTrialStatus` Current trial status for the identified user. Re-exported from the schema's UserTrialStatus — uses the canonical snake_case field names (`in_trial`, `trial_type`, etc.) to match the wire format and avoid brittle remapping. # RevTurbineTriggerEvent > **RevTurbineTriggerEvent** = `TriggerEventType` Canonical trigger event names recognised by the SDK and decision engine. These correspond to the placement categories defined in the product requirements (trial lifecycle, usage/limits, feature gating, retention, expansion). # RevTurbineTriggerPayload > **RevTurbineTriggerPayload** = [`TrialTriggerPayload`](/revturbine-sdk/api/interfaces/trialtriggerpayload/) \| [`UsageTriggerPayload`](/revturbine-sdk/api/interfaces/usagetriggerpayload/) \| [`FeatureGateTriggerPayload`](/revturbine-sdk/api/interfaces/featuregatetriggerpayload/) \| [`PaymentTriggerPayload`](/revturbine-sdk/api/interfaces/paymenttriggerpayload/) \| [`SdkEventProperties`](/revturbine-sdk/api/type-aliases/sdkeventproperties/) Union of all typed trigger payloads. # RevTurbineUiPathActionTypes > **RevTurbineUiPathActionTypes**\<`TUiPaths`\> = `Extract`\<`{ [Index in keyof TUiPaths]: TUiPaths[Index] extends { action_type: infer ActionType } ? ActionType : never }`\[`number`\], `string`\> Extracts `action_type` string literals from a UI path array type. ## Type Parameters ### TUiPaths `TUiPaths` *extends* readonly `unknown`[] # RevTurbineUiPathResolver > **RevTurbineUiPathResolver** = (`uiPath`) => `void` \| `Promise`\<`void`\> Resolver function for a UI path action type. ## Parameters ### uiPath `JsonObject` ## Returns `void` \| `Promise`\<`void`\> # RevTurbineUiPathResolverMap > **RevTurbineUiPathResolverMap** = `Record`\<`string`, `NonNullable`\<[`RevTurbineUiPathResolver`](/revturbine-sdk/api/type-aliases/revturbineuipathresolver/)\>\> Map of `action_type` -> resolver implementation supplied by the customer app. # RevTurbineUpdateInput > **RevTurbineUpdateInput** = `Omit`\<[`RevTurbineUserContext`](/revturbine-sdk/api/interfaces/revturbineusercontext/), `"id"` \| `"usage"`\> & `object` Input to [RevTurbineCustomerSdk.update](/revturbine-sdk/api/classes/revturbinecustomersdk/#update) — the advertised `update(patch)` verb. Accepts **every session-scoped user-context field** — everything on [RevTurbineUserContext](/revturbine-sdk/api/interfaces/revturbineusercontext/) except the identity handle `id` (and the persistence envelope `tenant_id` / `user_id` / timestamps, which the input type never carried). Those are bound at [RevTurbineCustomerSdk.identify](/revturbine-sdk/api/classes/revturbinecustomersdk/#identify) time and are not patchable here; use `identify()` to (re)establish identity. Every field is optional and merged with upsert semantics — an omitted field leaves the currently-held value untouched, so a partial patch (e.g. just `{ plan }`) never clobbers other context. Patchable fields include `plan`, `email`, `account_id`, `entitlements`, `custom`, `personalization`, `trial`, the billing-recovery signals (`payment_failed` / `payment_at_risk`), `tiers`, app-known experiment assignments (`experiments`, plan 183), and `usage`. Server-assigned fields (`seat_type_handle`, `activity_score`, `activity_score_computed_at`, `builtin_dimensions`) are not patchable (D-36, D-46; BL-0352, BL-0381): built-in dimension values reach the browser only from the authenticated client-context delivery. `usage` keeps its friendly `Record` shape (absolute balances), distinct from the richer per-entry `usage` on [RevTurbineUserContext](/revturbine-sdk/api/interfaces/revturbineusercontext/). ## Type Declaration ### usage? > `optional` **usage?**: [`UsageBalances`](/revturbine-sdk/api/type-aliases/usagebalances/) Usage balances to merge into the current snapshot (absolute values). # RevTurbineUsageSnapshot > **RevTurbineUsageSnapshot** = `Record`\<`string`, [`RevTurbineUsageSnapshotEntry`](/revturbine-sdk/api/interfaces/revturbineusagesnapshotentry/)\> Map of usage unit -> usage snapshot. # SdkEventProperties > **SdkEventProperties** = `Record`\<`string`, `JsonValue`\> Properties attached to analytics events. # SdkMetadata > **SdkMetadata** = `Record`\<`string`, `JsonValue`\> Extensible metadata bag attached to placements, events, and interactions. # SdkSessionOptions > **SdkSessionOptions** = [`RevTurbineInitInputOptions`](/revturbine-sdk/api/type-aliases/revturbineinitinputoptions/) & `object` Options for [initRevTurbine](/revturbine-sdk/api/functions/initrevturbine/), extending the standard init options with optional bootstrap placements. ## Type Declaration ### bootstrapPlacements? > `optional` **bootstrapPlacements?**: `object`[] Placements to bootstrap (preload decisions) on creation. # SdkTraits > **SdkTraits** = `Record`\<`string`, `JsonValue`\> User-provided traits or custom context (inherently untyped from customer code). # SeatAssignmentErrorReason > **SeatAssignmentErrorReason** = `"invalid_request"` \| `"unauthorized"` \| `"forbidden"` \| `"unknown_seat_type"` \| `"unknown_dimension_value"` \| `"request_failed"` Why a seat-type assignment was refused, derived from the HTTP status of `POST /api/sdk/user-contexts`: - `invalid_request` (400) — malformed body, e.g. an empty user id. - `unauthorized` (401) — missing or invalid credential. - `forbidden` (403) — the credential is not a **server** key. Only a server key may write a seat assignment (D-36). - `unknown_seat_type` (422) — the handle is not one of the tenant's current seat types. - `unknown_dimension_value` (422, code `UNKNOWN_DIMENSION_VALUE`) — a `builtinDimensions` value passed alongside the seat is outside its dimension's vocabulary (BL-0382). - `request_failed` — any other non-2xx status. # ServerActionHandler > **ServerActionHandler** = (`context`) => `Promise`\<[`ServerActionResult`](/revturbine-sdk/api/interfaces/serveractionresult/)\> App handler that calls its own backend for a server-owned mutation. ## Parameters ### context [`ServerActionContext`](/revturbine-sdk/api/interfaces/serveractioncontext/) ## Returns `Promise`\<[`ServerActionResult`](/revturbine-sdk/api/interfaces/serveractionresult/)\> # ServerActionMap > **ServerActionMap** = `Record`\<`string`, [`ServerActionHandler`](/revturbine-sdk/api/type-aliases/serveractionhandler/)\> Map of authored CTA action type to app-owned server mutation handler. # ServerEntitlementResult > **ServerEntitlementResult** = `ServerEvaluationPayloadEntitlementsValue` An entitlement check result within a server evaluation payload. # ServerEvaluationHydrationPayload > **ServerEvaluationHydrationPayload** = `GeneratedServerEvaluationPayload` Payload shape accepted by [RevTurbineCustomerSdk.hydrate](/revturbine-sdk/api/classes/revturbinecustomersdk/#hydrate). This is a re-export of the generated `ServerEvaluationPayload` from the schema, which is the contract between the server-side SDK's `evaluate()` output and the client-side SDK's hydration input. # ServerPlacementDecision > **ServerPlacementDecision** = `ServerEvaluationPayloadDecisionsItem` A single placement decision within a server evaluation payload. # ServerUserContext > **ServerUserContext** = `ServerEvaluationPayloadUserContext` User context returned in a server evaluation payload. # SlotRouteParams > **SlotRouteParams** = `Readonly`\<`Record`\<`string`, `string` \| readonly `string`[] \| `undefined`\>\> Framework route params, as Next.js `useParams()` returns them (a catch-all param is an array of segments). # SurfaceSlotCategory > **SurfaceSlotCategory** = `"fixed"` \| `"gated"` \| `"triggered"` Surface slot category that controls rendering behavior: - `fixed` — Embedded in a page; always rendered inline when the user qualifies (e.g. a banner, an inline card, a quota meter). - `gated` — Appears on button clicks or when a feature is attempted (e.g. a modal gate, a feature-lock prompt). - `triggered` — Server-driven; rules are evaluated, user qualifies for a segment, and a surface template is rendered as a toast, modal, tooltip, etc. # TelemetryConsent > **TelemetryConsent** = `"granted"` \| `"denied"` \| `"pending"` Behavior-telemetry consent state (plan 144 TASK-8 / REQ-11). - `granted` — emit normally (the default when the `telemetry` option is omitted). - `denied` — prevent event **creation**; nothing reaches ingest, consumers, or integrations. - `pending` — like `denied` in MVP: events are dropped and never persisted. # ToastComponentProps > **ToastComponentProps** = [`PlacementSlotProps`](/revturbine-sdk/api/interfaces/placementslotprops/) & `object` Props for ToastSlot. Extends [PlacementSlotProps](/revturbine-sdk/api/interfaces/placementslotprops/) with toast-specific options. ## Type Declaration ### duration? > `optional` **duration?**: `number` Auto-dismiss duration in seconds. Set to `0` to disable. Default `5`. ### toastPosition? > `optional` **toastPosition?**: `"top-right"` \| `"bottom-right"` \| `"bottom-center"` Screen position for the toast notification. Default `'bottom-right'`. # TrackFn > **TrackFn** = (`name`, `data?`, `options?`) => `void` The tracking function returned by [useTrack](/revturbine-sdk/api/functions/usetrack/). ## Parameters ### name `string` ### data? [`SdkEventProperties`](/revturbine-sdk/api/type-aliases/sdkeventproperties/) ### options? [`TrackOptions`](/revturbine-sdk/api/interfaces/trackoptions/) ## Returns `void` # TrackPurpose > **TrackPurpose** = `"engagement"` \| `"conversion"` \| `"operational"` \| `"diagnostic"` \| `string` & `object` Advisory event purpose (plan 144 TASK-12 / REQ-28). A **server-side allowlist** — never this client-declared value — decides whether an event feeds engagement scoring. The listed values are suggestions; any string is accepted. # UnvalidatedConfigArtifact > **UnvalidatedConfigArtifact** = `{ [K in typeof REQUIRED_BODY_ARRAY_FIELDS[number]]: unknown[] }` & `object` A config artifact as raw parsed JSON — what `import playbook from './revturbine.playbook.json'` produces. This exists because TypeScript widens JSON modules to plain `string`/`number` property types, which can never satisfy the literal-typed [ConfigArtifact](/revturbine-sdk/api/type-aliases/configartifact/) (`artifact_type: "playbook"` etc.), so a strict-mode project could not pass the imported JSON without a cast. Widening affects only *literal* types, so it is no reason to give up on the rest of the shape. The body arrays are structurally required here, which makes a truncated artifact a **compile** error at the `localRuntime` boundary rather than a runtime one — the whole point of plan 233. The header fields stay unconstrained because those are exactly the widened ones. The required-field list is *derived* from REQUIRED\_BODY\_ARRAY\_FIELDS, the same constant [normalizeConfigArtifactOrThrow](/revturbine-sdk/api/functions/normalizeconfigartifactorthrow/) validates against at runtime, so the compile-time and runtime contracts cannot drift apart. The index signature keeps every other field open: a Playbook carries many optional header fields, and a *fetched* artifact (`await res.json()`, typed `any`) still satisfies this, so the served path is not made harder to write — only harder to get silently wrong. Values are still validated at runtime by [normalizeConfigArtifactOrThrow](/revturbine-sdk/api/functions/normalizeconfigartifactorthrow/), which fails fast with a descriptive error; this type constrains the *shape*, never the values. # UsageBalances > **UsageBalances** = `Record`\<`string`, `number`\> Map of entitlement handle → current usage count. # UsePlacementOptions > **UsePlacementOptions** = `object` Options for the [usePlacement](/revturbine-sdk/api/functions/useplacement/) hook. ## Properties ### autoLoad? > `optional` **autoLoad?**: `boolean` Automatically load a decision on mount. Default `true`. *** ### autoTrackImpression? > `optional` **autoTrackImpression?**: `boolean` Whether a visible decision auto-tracks a resolution-time impression. Default `true`. Set `false` to suppress it — e.g. when you attach [UsePlacementResult.exposureRef](/revturbine-sdk/api/type-aliases/useplacementresult/#exposureref) and want the impression to fire on viewport exposure instead (plan 144 TASK-9 / REQ-15). Read at mount. *** ### contextMode? > `optional` **contextMode?**: `RevTurbineContextMode` Context resolution mode. Default `'auto'`. *** ### overrides? > `optional` **overrides?**: `RevTurbinePlacementDecisionOverrides` Override segment, plan, or usage for testing. *** ### placement? > `optional` **placement?**: [`RevTurbinePlacementConfig`](/revturbine-sdk/api/interfaces/revturbineplacementconfig/) Placement configuration (name, scope key, metadata). *** ### placementExposure? > `optional` **placementExposure?**: `PlacementExposureMode` When the presentation-writing `impression` fires (plan 144 TASK-11 / REQ-17). `legacy_resolution` (default) fires at resolution as today; `render` at render; `viewport` on viewport exposure (falling back to resolution when `IntersectionObserver` is unavailable). Only `viewport` moves the dashboard denominator — attach [UsePlacementResult.exposureRef](/revturbine-sdk/api/type-aliases/useplacementresult/#exposureref) to the visual root to use it. Read at mount. *** ### surfaceSlot? > `optional` **surfaceSlot?**: [`RevTurbineSurfaceSlotConfig`](/revturbine-sdk/api/interfaces/revturbinesurfaceslotconfig/) Canonical surface slot configuration (preferred over `placement`). *** ### traits? > `optional` **traits?**: `Record`\<`string`, `string` \| `number` \| `boolean`\> Custom traits to include in the decision request. *** ### ttlMs? > `optional` **ttlMs?**: `number` Decision cache TTL in milliseconds. *** ### userId? > `optional` **userId?**: `string` Target user ID. Falls back to the SDK's current user context. # UsePlacementResult > **UsePlacementResult** = `object` Result returned by the [usePlacement](/revturbine-sdk/api/functions/useplacement/) hook. ## Properties ### exposureBasis > **exposureBasis**: `ExposureBasis` \| `null` How the placement's visual root was exposed, or `null` before it enters the viewport (plan 144 TASK-9). `'render_fallback'` when `IntersectionObserver` is unavailable (AC-10). Only meaningful once [UsePlacementResult.exposureRef](/revturbine-sdk/api/type-aliases/useplacementresult/#exposureref) is attached. *** ### exposureRef > **exposureRef**: (`element`) => `void` Ref callback to attach to the placement's true visual root (plan 144 TASK-9). RevTurbine observes when that element enters the viewport for viewport-qualified exposure; when `IntersectionObserver` is unavailable it falls back to render (`exposure_basis: 'render_fallback'`, AC-10). Attaching it is optional and never adds a wrapper element (REQ-18/REQ-19). #### Parameters ##### element `Element` \| `null` #### Returns `void` # UserContextInput > **UserContextInput** = `Omit`\<`UserContext`, `"id"` \| `"tenant_id"` \| `"user_id"` \| `"created_at"` \| `"updated_at"`\> Convenience input type for identify / setUserContext calls. Omits the persistence envelope fields (`tenant_id`, `user_id`, `created_at`, `updated_at`) which the SDK populates automatically. ## Example ```ts sdk.identify('user_123', { account_id: 'acct_456', email: 'jane@acme.com', plan_handle: 'pro', entitlements: { data_export: true }, custom: { role: 'editor' }, }); ``` # CHECKOUT_STARTED_CTA_ACTION_TYPES > `const` **CHECKOUT\_STARTED\_CTA\_ACTION\_TYPES**: readonly `string`[] CTA actions that open a plan picker or pricing page rather than checkout itself. A `cta_clicked` on an output whose CTA is one of these emits `checkout_started` (plan 282 TASK-8/9) — the step between the click and checkout, so the placement chain can hold `success ≤ checkout_started ≤ clicked ≤ presented`. `open_checkout_modal` opens checkout directly ("on click") and is deliberately NOT listed: that placement has no separate step, so its started count equals its clicked count. `open_upgrade_modal` opens another placement whose own CTA then decides which of the two it is, so it is not listed either. `view_plans` is the authored alias the local resolver normalizes to `navigate_to_plans`; it is kept for an output built by hand. # CONTROL_PLANE_EVENT_SOURCE > `const` **CONTROL\_PLANE\_EVENT\_SOURCE**: `Record`\<`ControlPlaneEventType`, `ControlPlaneEventSource`\> Canonical source classification for every control-plane event type — mirrors the scaffold taxonomy. `system` covers identity/auth + CLI command telemetry; `workflow` covers playbook-version lifecycle, playbook transfer, and product-entity actions. Typed as a total `Record`, so adding a new `ControlPlaneEventType` in scaffold without classifying it here is a compile error. # CONTROL_PLANE_SOURCE_KEY > `const` **CONTROL\_PLANE\_SOURCE\_KEY**: `"control_plane_source"` = `'control_plane_source'` Property key under which [buildControlPlaneEvent](/revturbine-sdk/api/functions/buildcontrolplaneevent/) stamps the source classification on an emitted event's properties bag. # DARK_THEME > `const` **DARK\_THEME**: `Readonly`\<[`RevTurbineTheme`](/revturbine-sdk/api/interfaces/revturbinetheme/)\> Dark counterpart to [DEFAULT\_THEME](/revturbine-sdk/api/variables/default_theme/). Plan 233 TASK-6. The SDK had no sanctioned dark story at all, so a customer running a dark app either lived with white modals or — as the escalated one did — injected a palette into the Playbook object and re-initialized the SDK on every toggle. Shipping the palette is what makes `colorScheme` a swap rather than a rebuild. Only the colors differ. Typography, shape and shadows are scheme-independent, so they are inherited from [DEFAULT\_THEME](/revturbine-sdk/api/variables/default_theme/) rather than duplicated — a second copy would drift the moment either is edited. Contrast: every text-on-surface pair here clears WCAG AA at body size against its intended background. # DEFAULT_BRANDING > `const` **DEFAULT\_BRANDING**: `Required`\<`Pick`\<`BrandingConfig`, `"workspace_name"`\>\> & `BrandingConfig` Structural default branding. Neutral, brandless values that let the SDK render completely when no branding is supplied at any rung of the ladder. # DEFAULT_HOSTED_ENDPOINT > `const` **DEFAULT\_HOSTED\_ENDPOINT**: `"https://revturbine.com/app"` = `'https://revturbine.com/app'` Hosted-mode endpoint used when `endpoint` is omitted. Every hosted integration talks to the same control plane, so the option is a knob for proxies and self-hosted edges, not a requirement. # DEFAULT_SDK_MODE > `const` **DEFAULT\_SDK\_MODE**: [`RevTurbineSdkMode`](/revturbine-sdk/api/type-aliases/revturbinesdkmode/) = `'snippet'` Integration mode used when `mode` is omitted. `mode` labels telemetry (`page_view.mode`, diagnostics) and changes no behavior; `` passes `'react'`, everything else is a snippet. # DEFAULT_THEME > `const` **DEFAULT\_THEME**: `Readonly`\<[`RevTurbineTheme`](/revturbine-sdk/api/interfaces/revturbinetheme/)\> Default theme tokens — matches the original hardcoded styles across all built-in placement components. Serves as the fallback when no custom theme is configured or the theme API is unreachable. # EMITTABLE_PLATFORM_EVENT_NAMES > `const` **EMITTABLE\_PLATFORM\_EVENT\_NAMES**: readonly `string`[] Runtime mirror of [EmittablePlatformEventName](/revturbine-sdk/api/type-aliases/emittableplatformeventname/), derived from the payload-contract map so the emit surface and the taxonomy cannot drift — the parity test asserts this set plus the control-plane surface IS the taxonomy. # FALLBACK_ACCOUNT_ID_PREFIX > `const` **FALLBACK\_ACCOUNT\_ID\_PREFIX**: `"user-fallback:"` = `'user-fallback:'` Namespace marker in front of an `account_id` the SDK **fabricated** from the user id because the integration identified no account. Stable wire contract: analytics read-time guards match on this literal, so changing it is a breaking change to every pipe that filters on it. Callers should compare with [isFallbackAccountId](/revturbine-sdk/api/functions/isfallbackaccountid/) rather than re-deriving the string. ## Example ```ts import { FALLBACK_ACCOUNT_ID_PREFIX } from '@revturbine/sdk'; FALLBACK_ACCOUNT_ID_PREFIX; // 'user-fallback:' ``` # FIXED_BANNER_TEMPLATE_IDS > `const` **FIXED\_BANNER\_TEMPLATE\_IDS**: readonly `string`[] Banner templates only — for fixed or triggered banner slots. # FIXED_SURFACE_TEMPLATE_IDS > `const` **FIXED\_SURFACE\_TEMPLATE\_IDS**: readonly `string`[] Default template IDs accepted by `FixedSurfaceSlot`. # GATED_SURFACE_TEMPLATE_IDS > `const` **GATED\_SURFACE\_TEMPLATE\_IDS**: readonly `string`[] Default template IDs accepted by `AccessGateSurfaceSlot`. # GENERAL_BANNER_TEMPLATE_IDS > `const` **GENERAL\_BANNER\_TEMPLATE\_IDS**: readonly `string`[] General banner slot — accepts banners and in-page cards. # GENERAL_MODAL_TEMPLATE_IDS > `const` **GENERAL\_MODAL\_TEMPLATE\_IDS**: readonly `string`[] General modal slot — accepts modal overlays. # GENERAL_TOAST_TEMPLATE_IDS > `const` **GENERAL\_TOAST\_TEMPLATE\_IDS**: readonly `string`[] General toast slot — accepts toast messages. # INIT_STATUS_OK > `const` **INIT\_STATUS\_OK**: [`RevTurbineInitStatus`](/revturbine-sdk/api/interfaces/revturbineinitstatus/) The healthy status. Frozen so a consumer cannot mutate the shared default. # MESSAGE_SURFACE_TEMPLATE_IDS > `const` **MESSAGE\_SURFACE\_TEMPLATE\_IDS**: readonly `string`[] Default template IDs accepted by `MessageSurfaceSlot`. # MessageSurfaceSlot > `const` **MessageSurfaceSlot**: `any` Message surface slot — renders push-style notifications, modals, and toasts. Evaluates placement decisions based on configured triggers and renders the result as a notification, pop-up modal, or toast. The placement only appears when the decision engine returns a match for the current user context. For `on_event` triggering, use the forwarded ref: ## Example ```tsx // Mount-triggered notification (evaluates once on mount) // Event-triggered gate modal const ref = useRef(null); // Interval-polled notification ``` # RECOGNIZED_UPDATE_KEYS > `const` **RECOGNIZED\_UPDATE\_KEYS**: readonly \[`"usage"`, `"addons"`, `"account_id"`, `"email"`, `"email_type"`, `"plan_handle"`, `"plan"`, `"trial"`, `"payment_failed"`, `"payment_at_risk"`, `"tiers"`, `"entitlements"`, `"instances"`, `"custom"`, `"personalization"`, `"experiments"`, `"derived_config_version"`, `"context_hash"`, `"derived_computed_at"`\] Runtime mirror of [RevTurbineUpdateInput](/revturbine-sdk/api/type-aliases/revturbineupdateinput/)'s keys — the fields `update()` patches. `satisfies` guards each entry against typos/renames; `update-keys-exhaustiveness.test-d.ts` (compiled by `pnpm check:types:exact`) derives the reverse direction, so a schema-added context field fails the build until it is acknowledged here or omitted from the browser input types (BL-0352). JS callers' keys outside this set are dropped with a warning rather than merged into the context. # RuntimeMode > `const` **RuntimeMode**: `object` Runtime-mode values (plan 144). Reference these constants instead of raw string literals when setting RevTurbineInitOptions.runtimeMode — e.g. `runtimeMode: RuntimeMode.LocalOnly`. ## Type Declaration ### CustomEndpoints > `readonly` **CustomEndpoints**: `"custom_endpoints"` = `'custom_endpoints'` Customer-provided endpoint replacements. ### LocalOnly > `readonly` **LocalOnly**: `"local_only"` = `'local_only'` No server calls — runtime data is initialized locally from RevTurbineInitOptions.localRuntime. Emits **no telemetry** (neither the `/api/track` clickstream nor the keyless `/api/sdk/meta` beacon), which makes it the correct mode for demos and examples so they never report events. ### Server > `readonly` **Server**: `"revturbine_server"` = `'revturbine_server'` Standard RevTurbine-hosted endpoints (the default). # SERVER_TRAITS_DOMAIN > `const` **SERVER\_TRAITS\_DOMAIN**: `"traits:server"` The `traits:server` domain name — a `TraitsNamespace`. # SLOT_ROUTE_ID_PLACEHOLDER > `const` **SLOT\_ROUTE\_ID\_PLACEHOLDER**: `":id"` = `':id'` The placeholder an identifier-like path segment is templated to. # SLOT_ROUTE_MAX_LENGTH > `const` **SLOT\_ROUTE\_MAX\_LENGTH**: `512` = `512` Upper bound on an emitted route — matches scaffold `SURFACE_SLOT_ROUTE_MAX_LENGTH`. # Component Gallery > Interactive demos of all built-in placement slot components with live theme switching. The Component Gallery renders each built-in placement slot directly in the browser — no Sandpack or iframe needed. Switch between story variants and theme presets to see how each component responds. ## Available Components | Component | Type | Description | |-----------|------|-------------| | [ButtonSlot](/revturbine-sdk/components/button-slot/) | `button` | Standalone CTA button for nav bars and inline prompts | | [BannerSlot](/revturbine-sdk/components/banner-slot/) | `banner` | Full-width banner for announcements and usage warnings | | [ModalSlot](/revturbine-sdk/components/modal-slot/) | `modal` | Overlay modal for feature gates and trial expirations | | [ToastSlot](/revturbine-sdk/components/toast-slot/) | `toast` | Ephemeral toast notification for usage alerts | | [QuotaMeterSlot](/revturbine-sdk/components/quota-meter-slot/) | `in_page` | Usage meter with progress bar, counter, and gauge styles | | [InlineEmbedSlot](/revturbine-sdk/components/inline-embed-slot/) | `in_page` | Inline card for feature gates and upgrade prompts | Each page includes: - **Variant selector** — switch between story variants (e.g. Primary, Secondary, Accent) - **Theme selector** — Default, Dark, and Warm Brand presets - **Live rendering** — components render with the SDK's real theme system # BannerSlot > Full-width banner placement — announcements, usage warnings, and promotional messages. A full-width banner for announcements, usage warnings, and time-sensitive promotions. Supports top/bottom positioning and optional dismiss. **Content fields:** `header`, `body`, `cta_label`, `position` (`top` | `bottom`), `dismissible` ## Usage A live, editable example — edit the code (or switch the demo user) and the preview re-runs: ```tsx import { Slot } from "@revturbine/sdk"; import { DemoApp } from "./DemoApp"; // Bob is on Professional — the engine serves him an annual-upgrade banner. export default function App() { return ( ); } ``` The `BannerSlot` renders automatically when the decision engine selects a `banner_placement` template. Banners render at the top or bottom of the page. When `dismissible` is `true`, users can close the banner. The SDK tracks dismissals to prevent re-showing. **Related:** [Playground → Annual Banner](/revturbine-sdk/playground/#fixed-banner) · [API Reference](/revturbine-sdk/api/) # ButtonSlot > Standalone CTA button placement — renders a styled upgrade/action button. A standalone CTA button suitable for persistent nav CTAs or inline upgrade prompts. **Content fields:** `cta_label`, `style` (`primary` | `secondary` | `accent`) ## Usage A live, editable example. The `` renders whatever `button` placement the decision engine picks for the current user — or nothing. Edit the code (or switch the demo user) and the preview re-runs: ```tsx import { Slot } from "@revturbine/sdk"; import { DemoApp } from "./DemoApp"; // Carol is on Starter with an active trial, so the engine serves her an upgrade // button. Switch to user="user_eve" (Enterprise) and the slot renders nothing — // there's nothing to upsell. export default function App() { return ( ); } ``` The `ButtonSlot` renders automatically when the decision engine selects a `button` template for the slot. Style variants are controlled via the `style` content field in your placement configuration. **Related:** [Playground → Upgrade Button](/revturbine-sdk/guides/placements/#fixed-button) · [API Reference](/revturbine-sdk/api/) # InlineEmbedSlot > Inline card placement — feature gates and upgrade prompts embedded in page flow. An inline card for feature gates and contextual upgrade prompts. Embeds naturally in the page flow alongside content. **Content fields:** `header`, `body`, `cta_label`, `image_url`, `message` ## Usage A live, editable example. The user below is **not** entitled, so the gate renders the configured inline upgrade card in place of the children — edit the code (or switch to an entitled demo user) and the preview re-runs: ```tsx import { Gate } from "@revturbine/sdk"; import { DemoApp } from "./DemoApp"; // Dan lacks the branding entitlement, so the gate renders the configured inline // upgrade card. Switch to user="user_alice" (entitled) to see the children. export default function App() { return (
Branding controls — visible only when entitled
); } ``` The `InlineEmbedSlot` renders automatically when the decision engine selects an `inline_gate_message` template. Inline embeds render in-flow where you place them. When the entitlement is denied, the gate content replaces the protected component. **Related:** [Playground → Branding Gate](/revturbine-sdk/playground/#gate-inline) · [API Reference](/revturbine-sdk/api/) # ModalSlot > Overlay modal placement — feature gates, trial endings, and upgrade prompts. An overlay modal for feature gates, trial expirations, and high-impact upgrade prompts. Supports optional (dismissible) and blocking modes. **Content fields:** `header`, `body`, `cta_label`, `secondary_cta_label`, `image_url`, `style` (`blocking`) ## Usage A live, editable example. The user below is **not** entitled, so the gate renders the configured upgrade modal in place of the children — edit the code (or switch to an entitled demo user) and the preview re-runs: ```tsx import { Gate } from "@revturbine/sdk"; import { DemoApp } from "./DemoApp"; // Dan lacks the data_export entitlement, so the gate renders the configured // upgrade modal. Switch to user="user_alice" (entitled) to see the children. export default function App() { return (
Export panel — visible only when entitled
); } ``` The `ModalSlot` renders automatically when the decision engine selects a `modal_overlay` template. Blocking modals have no dismiss option — the user must take action. Optional modals include a secondary CTA or close button. **Related:** [Playground → Data Export Gate](/revturbine-sdk/guides/entitlements/#gate-modal) · [API Reference](/revturbine-sdk/api/) # QuotaMeterSlot > Usage meter placement — progress bars, numeric counters, and circular gauges. A usage meter that visualizes consumption against limits. Supports progress bar, numeric counter, and circular gauge display styles with automatic color thresholds. **Content fields:** `header`, `usage_current`, `usage_limit`, `cta_label`, `display_style` (`progress_bar` | `numeric_counter` | `circular_gauge`), `show_at` ## Usage A live, editable example — edit the code (or switch the demo user) and the preview re-runs: ```tsx import { Slot } from "@revturbine/sdk"; import { DemoApp } from "./DemoApp"; // Alice has consumed 20 of her 30 core credits — the meter renders her usage. export default function App() { return ( ); } ``` The `QuotaMeterSlot` renders automatically when the decision engine selects a `usage_counter` template. The meter changes color automatically based on usage percentage — green below 70%, amber at 70-90%, red above 90%. The `show_at` threshold hides the meter until usage reaches that percentage. **Related:** [Playground → Quota Meter](/revturbine-sdk/playground/#fixed-usage-counter) · [API Reference](/revturbine-sdk/api/) # ToastSlot > Ephemeral toast notification — non-blocking alerts for usage events and feature announcements. A non-blocking toast notification for usage alerts, feature announcements, and success confirmations. Supports multiple positions and optional CTA. **Content fields:** `message`, `cta_label`, `position` (`bottom-right` | `top-right` | `bottom-center`), `duration` ## Usage A live, editable example — edit the code (or switch the demo user) and the preview re-runs: ```tsx import { Slot } from "@revturbine/sdk"; import { DemoApp } from "./DemoApp"; // Bob is on Professional — the engine serves him a trial-welcome toast. export default function App() { return ( ); } ``` The `ToastSlot` renders automatically when the decision engine selects a `toast_message` template. Toasts auto-dismiss after `duration` milliseconds (set to `0` to disable auto-dismiss). Position is controlled by the `position` content field. **Related:** [Playground → Trial Toast](/revturbine-sdk/playground/#msg-toast) · [API Reference](/revturbine-sdk/api/) # Architecture > SDK architecture, directory structure, and key concepts. ## Directory Structure ```text sdk/ ├── index.ts # Barrel export (entry point for @revturbine/sdk) ├── customer-side.ts # Core SDK class — identity, placements, entitlements, events ├── generated.ts # Re-exports the bundled schema types (generated) ├── react/ # React integration layer │ ├── RevTurbineProvider # Context provider — wraps app for SDK access │ ├── usePlacement # Hook — load decision + manage interactions │ └── Placement # Render-prop alternative to usePlacement ├── placements/ # Placement rendering system │ ├── types.ts # Slot type interfaces │ ├── registry.ts # PlacementTypeRegistry — resolves outputs → slot components │ ├── builtin.ts # Registers all 9 built-in slot types │ ├── PlacementRenderer # Core renderer — resolves slot type, expands tokens, renders │ ├── Slot # Drop-in component (decision + render in one step); exported as SurfaceSlotComponent too │ ├── useSurfaceSlot # Hook variant of │ └── slots/ # Built-in slot components │ ├── BannerSlot # Full-width sticky banner (top/bottom) │ ├── ModalSlot # Centered overlay dialog │ ├── InlineEmbedSlot # Inline card in page flow │ ├── ToastSlot # Ephemeral auto-dismiss notification │ ├── ButtonSlot # Single CTA button │ ├── FullPageSlot # Dedicated full-page (plans/upgrade) │ ├── QuotaMeterSlot # Usage meter (bar/gauge/numeric) │ ├── CliSlot # CLI-style monospace message │ └── CreditBalanceSlot # Depleting credit balance display ``` ## Where decisions run In every runtime mode a decision — which plan a user is on, whether they can use a feature, which placement a slot shows — is a pure function of the user context and the Playbook, evaluated inside your app by the SDK. Nothing in your request path calls RevTurbine to decide. The modes differ only in where the Playbook comes from and where telemetry goes: 1. **`revturbine_server`** (default) — the SDK fetches the launched Playbook from the RevTurbine control plane, keeps it cached, and re-checks for a new release about once a minute. Server-derived context (plan, trial, payment state) arrives through a client session; events go to RevTurbine ingest. 2. **`local_only`** — you bundle the Playbook with your app. No account, and no network on the decision path. Runtime state is persisted in `localStorage`. 3. **`custom_endpoints`** — the same traffic as `revturbine_server`, routed through endpoints you host. See [Runtime Modes](/revturbine-sdk/guides/runtime-modes/) for setup and cache lifetimes. ## Placement Type Registry The SDK uses a registry pattern to map decision engine outputs to React components: 1. Built-in slot types (banner, modal, toast, etc.) are registered at init. 2. Customers can register custom slot types via `useRegisterPlacementType`. 3. `PlacementRenderer` resolves outputs → slot types via template → surface type → fallback chain. ## Personalization Tokens Content fields support `{{token_name}}` syntax. The SDK resolves tokens against a `PersonalizationContext` before rendering: ```ts const personalization = { user_name: 'Jane', plan_name: 'Pro', usage_current: 8500, usage_limit: 10000, }; ``` ## Decision Lifecycle 1. `registerPlacement()` → deterministic placement ID 2. `getPlacementDecision()` → visibility + content + reason codes 3. `trackTreatmentInteraction()` → impression/dismiss/CTA tracking 4. Suppression rules prevent re-showing dismissed placements ## Provider Pattern The SDK supports an optional provider strategy for `getPlacement`, `checkEntitlement`, and `identify`: - **`provider`**: primary provider object or factory. - **`providerFallbacks`**: ordered list of fallback providers. - **`providerFailureSlotBehavior`**: slot behavior after provider-chain failure — `'invisible'` (default) or `'placeholder'`. Providers are an escape hatch for teams that supply their own facts (entitlement grants, user context) from more than one source; the default path needs none. When the primary provider fails, the SDK logs a warning and tries each configured fallback in order. If every configured provider for that method fails, slots render according to `providerFailureSlotBehavior` rather than guessing. See [Runtime Modes → Provider Fallback Strategy](/revturbine-sdk/guides/runtime-modes/#provider-fallback-strategy) for configuration details. ## Self-Documenting Exports All public SDK exports use **TSDoc** comments that are: 1. Shown inline in VS Code / IDE intellisense 2. Extracted by **TypeDoc** into the [API Reference](/revturbine-sdk/api/) 3. Picked up by **Storybook** for component documentation pages When adding or modifying SDK exports, include TSDoc with a summary sentence, `@example` code blocks, and `@param` / `@returns` for non-obvious function signatures. # Core Concepts > The smallest mental model for RevTurbine — three primitives (entitlements, gates, placements), the Playbook that configures them, user context, and where decisions run. RevTurbine has a small vocabulary. Learn three primitives and you can read the rest of the docs. ## The three primitives - **Entitlements — *who can do what*.** An entitlement answers "is this user allowed to do X, and how much is left?" Feature access, usage limits, credits, and seats are all entitlements. You check them with [`can()`](/revturbine-sdk/getting-started/api-overview/) / `checkEntitlement`. - **Gates — *where your app asks*.** A gate is the checkpoint in your code or UI where you consult an entitlement before letting something happen — a `` around a subtree, a `gate('export', …)` around an action, or a bare `can()` check. Same question, different shapes. - **Placements — *what to show when it matters*.** A placement is authored presentation that the SDK selects locally: an upgrade prompt when a gate denies, a "running low" banner near a limit, a trial-ending toast. You render one with a [``](/revturbine-sdk/getting-started/api-overview/). That's the whole loop: an **entitlement** defines the policy, a **gate** asks the question in your app, and a **placement** is what RevTurbine shows when the answer should change behavior. ## What configures them: the Playbook All three primitives are driven by your **Playbook** — the portable config RevTurbine evaluates. It holds the plans, entitlement rules (feature / usage-limit / credits / seats), trials, segments (targeting), and placements. A PM edits the Playbook in the RevTurbine dashboard and republishes; your app code doesn't change. ## User context RevTurbine decides for a *specific* user, so you tell it who's asking: an id, the user's current plan handle, reported usage, and any custom traits. You set this with `rt.identify(userId, { plan_handle, usage, …})` or the `` `options.user`. Entitlement checks and [segment targeting](/revturbine-sdk/guides/experiments/) both read from it. The dedicated [`RevTurbineUserContext` guide](/revturbine-sdk/user-context/) covers every field, update semantics, and server-derived context. ## Runtime modes The same primitives work whether the SDK reads a Playbook bundled with your app (local mode, `local_only`), syncs it from RevTurbine (hosted mode, `revturbine_server`), or syncs it through your own endpoints (`custom_endpoints`). See [Runtime Modes](/revturbine-sdk/guides/runtime-modes/). ## Where decisions run **Tip — Local decisions, explicit delivery policy:** The RevTurbine SDK runs inside your app, and on your backend where you verify. It evaluates entitlements and placements locally, as a pure function of your Playbook and the current user context. Hosted loading and refresh can fail; choose an explicit freshness and failure policy for the snapshot your app evaluates. We push additional inputs to your local engine: aggregated frequency caps, prioritization scores as available. These are additive; nothing breaks or blocks if not present. A check that can't justify a grant returns no access rather than guessing, so a misconfiguration never hands out a paid feature. For the money-safe re-check, see [Client vs Server Enforcement](/revturbine-sdk/concepts/enforcement/). ## Glossary | Term | Meaning | |---|---| | **Entitlement** | What a user is allowed to do / how much is left (feature, usage limit, credits, seats) | | **Gate** | A checkpoint where your app consults an entitlement (``, `gate()`, `can()`) | | **Placement** | The UI the decision engine shows at the right moment (rendered by ``) | | **Playbook** | The portable config RevTurbine evaluates (plans, entitlements, segments, placements) | | **Segment** | A named rule over user traits used for targeting and experiments | | **Slot** | The component that renders a placement (``) | | **User** | An application-identified person; `user.id` is distinct from their account | | **Account** | An organization or billing scope identified by `account_id`; membership is enforced by your app | | **Trigger** | A supported event or condition that makes a placement eligible for consideration | "Moment" can describe when your app asks for a decision, but SDK 0.12.4 has no public `moment()` function. Use the documented trigger, `Gate`, `Slot`, and headless APIs. Likewise, proposed `InlineSlot`/`OverlaySlot` names are not current exports. A slot is a place to render; an overlay is a presentation choice of the selected component/template, not a separate SDK decision service. For the component/config name aliases (`SurfaceSlotComponent`, `Playbook`, …), see the [Recommended API Path](/revturbine-sdk/getting-started/api-overview/#the-vocabulary-slot--gate--playbook). ## Related - [Recommended API Path](/revturbine-sdk/getting-started/api-overview/) — which call to reach for - [Client vs Server Enforcement](/revturbine-sdk/concepts/enforcement/) — policy vs enforcement - [Entitlements](/revturbine-sdk/guides/entitlements/) · [Placements](/revturbine-sdk/guides/placements/) — the primitives in depth # Client vs Server Enforcement > How RevTurbine splits responsibility — RevTurbine owns monetization policy, your app owns enforcement, a check that can't justify a grant denies, and billing-critical actions are re-checked on your server. Plus the difference between , gate(), and can(). Every team evaluating RevTurbine asks the same question: *"If entitlements are checked in the browser, what stops a user from bypassing them?"* The browser can be tampered with, so a client check is a UX hint, not the authority. Your backend re-runs the check at the moment it matters, immediately before a billable action. The re-check is the same SDK evaluating the same Playbook, on your server. It calls our servers only for additive data. Verify wherever being wrong costs you money; pure display gating doesn't need it. The rest of this page is the division of responsibility, and which tool to use where. ## The model, stated once - **RevTurbine is authoritative for monetization *policy*.** Which plans exist, what each plan entitles, when a placement shows — that lives in your [Playbook](/revturbine-sdk/getting-started/api-overview/#the-vocabulary-slot--gate--playbook) and the control plane, and a PM changes it with no code deploy. - **Your app owns *enforcement*.** RevTurbine tells you the policy answer; your app is what actually allows or blocks the action. - **No grant, no access.** An entitlement check that can't justify a grant denies rather than guessing, so a misconfiguration never *leaks* a paid feature ([the reason codes](/revturbine-sdk/guides/entitlements/#fail-closed-semantics) say why). - **Money-safe decisions belong on your server.** A browser check can be tampered with. For anything that costs money or invites abuse, re-run the check server-side before you grant access. ## Three tools, and where each runs The SDK gives you three ways to act on an entitlement. They sound alike; they are not the same. | Tool | Kind | Runs | Use it for | |---|---|---|---| | `` | React component | Client (UI) | Show/hide a subtree and render an upsell when denied | | `rt.gate(action, fn)` | Imperative verb | Client | Run `fn` only if entitled; get the entitlement back to open a paywall | | `rt.can(handle)` · `useCan` · `checkEntitlement` | Check | Client **or** server | Ask "is this allowed?" and get the full result — the one you re-run server-side | `` and `gate()` are convenience wrappers around a `can()`/`checkEntitlement()` call. All three run in the browser and deny whenever they can't affirm a grant. The check itself — `checkEntitlement` / `can` — is also what you run on your server (via the server SDK or your own logic) when the decision has to be trusted. **Caution — Custom resolvers own their network behavior:** The built-in evaluator decides from the loaded Playbook in memory, so an entitlement check makes no per-check network call. If you inject `localRuntime.resolvers`, your resolver can introduce network I/O; its caching, timeout, and failure behavior are your responsibility. The SDK still requires an affirmative result before granting access. ## When do I need the server? | Action | Client check enough? | |---|---| | Show or hide a button, badge, or menu item | ✅ Yes | | Render an upgrade prompt or paywall | ✅ Yes | | Branch UI copy or messaging | ✅ Yes | | Start a paid export, consume credits, provision a seat | ❌ Re-check on your server | | Anything a user could profit from by faking | ❌ Re-check on your server | The rule: **the client check drives the UI; your server makes the money-safe decision.** They use the same entitlement — you're just not trusting the browser for the part that matters. ## In practice Use the client check for a fast, friendly UX: ```tsx import { Gate } from '@revturbine/sdk'; // Client: instant show/hide + upsell when denied; no grant means no access. ``` Then verify again on your server before doing the paid work — with the same SDK, evaluating the same Playbook in-process: ```ts import { initRevTurbine, RuntimeMode } from '@revturbine/sdk/headless'; import playbook from './revturbine.playbook.json'; // Server: the trusted decision. The instance is built from a Playbook this // process already loaded, so the check makes no call to RevTurbine. export async function POST(request: Request): Promise { const user = await getSignedInUser(request); // your own auth const rt = await initRevTurbine({ runtimeMode: RuntimeMode.LocalOnly, localRuntime: { playbook }, user: { id: user.id, plan_handle: user.planHandle }, }); const result = await rt.can('data_export'); if (!result.allowed) return Response.json({ error: 'upgrade_required' }, { status: 402 }); return runExport(request); } ``` Two conventions worth keeping: answer a denied entitlement with **402** (upgrade to proceed) and reserve 403 for permission denials, so clients can tell the two apart; and build one instance per request from a Playbook you loaded once per process, because the SDK holds a single user context per instance. See [Server-Side Integration](/revturbine-sdk/getting-started/server-side/#headless-server-re-check) for why a server does not use hosted `Server` mode. The client `` keeps the UI honest and fast; the server check keeps the revenue safe. Both evaluate the same Playbook locally, so neither depends on our servers at decision time. ## Related - [Recommended API Path](/revturbine-sdk/getting-started/api-overview/) — when to reach for `can()` vs `gate()` vs `` - [Server-Side Integration](/revturbine-sdk/getting-started/server-side/) — running entitlement checks on your backend - [Entitlements](/revturbine-sdk/guides/entitlements/) — the entitlement model behind every check - [Error Handling](/revturbine-sdk/guides/error-handling/) — placement degradation and what a check returns when it can't justify a grant # Provider Architecture > How the SDK's provider pattern lets you supply custom placement, entitlement, and identity logic. By default the SDK evaluates placements and entitlements inside your app, from the Playbook — no provider is involved, and most apps never configure one. A **provider** is an escape hatch for teams that already have a placement or entitlement source of their own and want the SDK's rendering, tracking, caps, and cooldowns on top of it. A provider is a plain object that implements one or more domain methods — placement resolution, entitlement checks, placement type persistence, and user identification. ## The Provider Interface ```ts interface RevTurbineSdkProvider { getPlacement?: (config: RevTurbinePlacementRequestConfig) => Promise; checkEntitlement?: (handle: string, context?: RevTurbineEntitlementContext) => Promise; persistPlacementTypes?: (types: RevTurbinePlacementTypeEntity[]) => Promise; identify?: (userId: string, contextOrTraits?: UserContextInput | SdkTraits) => void; } ``` Every method is optional. Implement only the domains you need to customize — the SDK falls back to its built-in local runtime for anything you don't provide. ## Domain Methods ### `getPlacement` Resolves a placement for a given slot and surface type. Called by `` (and its `FixedSurfaceSlot` / `MessageSurfaceSlot` variants), ``, and the headless API. ```ts const analyticsProvider: RevTurbineSdkProvider = { async getPlacement(config) { // config.slotId — the slot requesting a placement // config.componentType — the component type (button, modal, banner, etc.) // config.entitlementHandle — for access gate slots // Example: a placement source you already run (escape hatch — the // default path evaluates the Playbook locally and needs no provider) const res = await fetch(`/api/placements/${config.slotId}`); if (!res.ok) return null; return res.json(); }, }; ``` **Input — `RevTurbinePlacementRequestConfig`:** | Field | Type | Description | |---|---|---| | `slotId` | `string?` | Slot identifier | | `componentType` | `RevTurbineComponentType?` | Component type (`button`, `modal`, `banner`, etc.) | | `entitlementHandle` | `string?` | Entitlement to check (access gates) | | `planHandle` | `string?` | Plan-specific placements | | `placementHandle` | `string?` | Chaining from a prior CTA path | **Return:** `PlacementOutput | null` — the resolved placement, or `null` if no placement matches. ### `checkEntitlement` Checks whether the current user has access to a feature. Called by `` and `sdk.checkEntitlement()`. ```ts const entitlementProvider: RevTurbineSdkProvider = { async checkEntitlement(handle, context) { // handle — e.g. "data_export", "brand_kit" // context — optional entitlement context const res = await fetch(`/api/entitlements/${handle}`); const data = await res.json(); return { status: data.allowed ? 'allowed' : 'denied', allowed: data.allowed, }; }, }; ``` **Return — `EntitlementResult`:** | Field | Type | Description | |---|---|---| | `status` | `string` | `'allowed'`, `'limited'`, or `'denied'` | | `allowed` | `boolean` | Whether access is granted | ### `persistPlacementTypes` Stores placement type metadata. Called during SDK initialization to register built-in and custom slot types. ```ts const storageProvider: RevTurbineSdkProvider = { async persistPlacementTypes(types) { // types — array of { id, label, description, componentType, priority } await fetch('/api/placement-types', { method: 'POST', body: JSON.stringify(types), }); }, }; ``` ### `identify` Called when a user is identified or their context changes. Use this for analytics, session tracking, or syncing identity to your backend. ```ts const identityProvider: RevTurbineSdkProvider = { identify(userId, contextOrTraits) { analytics.identify(userId, contextOrTraits); }, }; ``` ## Using a Provider Pass your provider via the `provider` option: ```tsx import { RevTurbineProvider, RuntimeMode } from '@revturbine/sdk'; import type { RevTurbineSdkProvider } from '@revturbine/sdk'; import playbook from './revturbine.playbook.json'; import { useMemo } from 'react'; // Typing the object as RevTurbineSdkProvider keeps `status` on the SDK's // literal union — without it TypeScript widens it to `string` and the // provider no longer satisfies the interface. const myProvider: RevTurbineSdkProvider = { async getPlacement(config) { const res = await fetch(`/api/placements/${config.slotId}`); if (!res.ok) return null; return res.json(); }, async checkEntitlement(handle) { const res = await fetch(`/api/entitlements/${handle}`); const data = await res.json(); return { status: data.allowed ? 'allowed' : 'denied', allowed: data.allowed }; }, }; function App() { const options = useMemo(() => ({ runtimeMode: RuntimeMode.LocalOnly, localRuntime: { playbook }, provider: myProvider, }), []); return ( {/* slots will call myProvider.getPlacement() */} ); } ``` ## Provider Factory If your provider needs access to the SDK's init options, use a factory function: ```ts const myProviderFactory = (options) => ({ async getPlacement(config) { // options.localRuntime, options.user, etc. are available here return null; }, }); const options = { runtimeMode: RuntimeMode.LocalOnly, localRuntime: { playbook }, provider: myProviderFactory, }; ``` ## Fallback Chain The SDK supports a chain of providers. If the primary provider returns `null` or throws, the next one is tried: ```tsx const options = useMemo(() => ({ runtimeMode: RuntimeMode.LocalOnly, localRuntime: { playbook }, provider: apiProvider, providerFallbacks: [cachedProvider, staticFallback], }), []); ``` If all providers fail: | Behavior | Result | |---|---| | `'invisible'` (default) | Slots render nothing | | `'placeholder'` | Slots show fallback content | An entitlement check that can't justify a grant denies — here with `{ allowed: false, reason: 'sdk_disabled_provider_failure' }`. Slot rendering on provider failure is governed separately, by the table above. ## Analytics Provider The SDK can forward all impressions, interactions, and lifecycle events to your analytics platform (Segment, Heap, Amplitude, PostHog, Mixpanel, or any custom sink). Use `createAnalyticsProvider()` to create a domain provider that receives every SDK event. ### Basic Setup ```tsx import { RevTurbineProvider, createAnalyticsProvider, RuntimeMode } from '@revturbine/sdk'; import playbook from './revturbine.playbook.json'; import { useMemo } from 'react'; function App() { const options = useMemo(() => { const analytics = createAnalyticsProvider({ handler: (eventName, properties) => { // Push to your analytics platform // `segment` here is your own analytics SDK (Segment, Amplitude, …). segment.track(eventName, properties); }, }); return { runtimeMode: RuntimeMode.LocalOnly, localRuntime: { playbook }, domainProviders: [analytics], }; }, []); return ( {/* All impressions and interactions now flow to Segment */} ); } ``` ### Events Captured The analytics provider receives every SDK event, including: | Event | When | |---|---| | `placement_interaction` | A placement is shown, clicked, or dismissed | | `placement_dismissed` | User closes a placement | | `placement_converted` | User completes a CTA | | `placement_snoozed` | User clicks "remind me later" | | `page_view` | Page navigation | | Trigger events | `trial_expiring`, `usage_limit_reached`, etc. | Each event includes `user_id`, `anonymous_id`, `session_id`, `placement_id`, `interaction_type`, `url`, `page_title`, and the full event properties. ### Filtering Events Only forward the events you care about: ```ts const analytics = createAnalyticsProvider({ handler: (eventName, properties) => { heap.track(eventName, properties); }, filter: ['placement_interaction', 'placement_dismissed', 'placement_converted'], }); ``` ### Transforming Events Rename events, enrich properties, or drop events with a `transform` function. Return `null` to suppress an event. ```ts const analytics = createAnalyticsProvider({ handler: (name, props) => posthog.capture(name, props), transform: (name, props) => ({ eventName: `revturbine.${name}`, properties: { ...props, source: 'revturbine-sdk' }, }), }); ``` ### Platform Examples **Segment:** ```ts const analytics = createAnalyticsProvider({ handler: (eventName, properties) => { segment.track(eventName, properties); }, }); ``` **Heap:** ```ts const analytics = createAnalyticsProvider({ handler: (eventName, properties) => { heap.track(eventName, properties); }, filter: ['placement_interaction', 'placement_dismissed', 'placement_converted'], }); ``` **Amplitude:** ```ts const analytics = createAnalyticsProvider({ handler: (eventName, properties) => { amplitude.track(eventName, properties); }, }); ``` **PostHog (with prefix):** ```ts const analytics = createAnalyticsProvider({ handler: (name, props) => posthog.capture(name, props), transform: (name, props) => ({ eventName: `revturbine.${name}`, properties: props, }), }); ``` ## Next Steps - [Runtime Modes](/revturbine-sdk/guides/runtime-modes/) — choosing the right runtime mode - [Component Gallery](/revturbine-sdk/components/) — interactive demos of every built-in slot - [API Reference](/revturbine-sdk/api/) — full TypeDoc reference for all exports # Runtime and Playbook > The two halves of every RevTurbine decision — the Playbook you author and the Runtime that holds who is asking — how they combine locally, and which pieces are shipped today. For the complete body inventory and reference relationships, see the [Playbook field map](/revturbine-sdk/reference/playbook-map/). Every RevTurbine decision, whether it is "may this user export?" or "which banner should this user see?", is one calculation with two inputs: - the **Playbook**: what you decided about plans, entitlements, targeting and messages. It is the same for every user, and you author it. - the **Runtime**: who is asking right now, and what they have used. It is different for every user and changes while they use your app. The SDK combines the two **locally**, in your app or on your server. Nothing in that calculation waits on RevTurbine. ```text Playbook (authored, versioned) Runtime (per user, per moment) plans · entitlements · segments UserContext · usage · app answers placements · payloads (AccountContext: planned) \ / \ / +---- SDK: local evaluation ----+ | entitlement result · winning placement | your UI renders it · your server enforces it ``` ## The Playbook The Playbook is the portable artifact RevTurbine evaluates: one JSON file, `revturbine.playbook.json`, that you edit in the dashboard, with the CLI or with your coding agent. Every launch is an immutable, versioned Release. In local mode you bundle the file; in hosted mode the SDK fetches the launched Release. Either way the SDK evaluates the same artifact the same way. | Primitive | In plain English | Why it exists | Example | |---|---|---|---| | **Plan** | A tier a customer can be on. A plan can have more than one price, each called a plan variation (monthly, annual or regional); variations share the plan's tier, so switching between them is never an upgrade. | So your app asks "what is this user entitled to?" instead of "are they on Pro?" | `starter`, `pro` | | **Entitlement** | One thing a user may be allowed to do, and how much: a feature, a usage limit, credits or seats. | One vocabulary for gating, metering and upgrade prompts. | `batch_export`, `api_calls` (limit 1,000) | | **Entitlement rule** | Says which plans, segments or seat types get an entitlement, and how much. | Changing who gets what is a Playbook edit, not a deploy. | "Pro gets `api_calls` up to 1,000 a month" | | **Segment** | A named group of users, defined by rules over their context. | Targeting and experiments: show this to trial users only. | `trial_users` | | **Placement** | A message the engine may show at the right moment: a banner, modal, toast or button. Its **payload** is the copy, offer and call to action. | Upgrade prompts are decided by caps, cooldowns and priority, not by your code. | A "you have 5 exports left" banner | Related terms you will meet: - An **add-on** is something a customer buys on top of a plan. - A **gate** is where your app asks an entitlement question (``, `gate()`, `can()`); a **slot** is where your app can show a placement (``). Both are covered in [Core Concepts](/revturbine-sdk/concepts/core-concepts/). ## The Runtime The Runtime is everything the SDK knows about the moment of the decision. | Piece | In plain English | Who supplies it | |---|---|---| | **UserContext** | The user: id, plan, usage balances, trial state, custom traits. | Your app at init and as things change, plus plan, trial and payment state from Stripe through a [client session](/revturbine-sdk/guides/client-sessions/) in hosted mode. See [`RevTurbineUserContext`](/revturbine-sdk/user-context/). | | **Usage** | How much the user has consumed. Always an absolute balance, never a delta. | Your app (`update({ usage })`). | | **App answers** | When your own system already decides an entitlement, you pass its answer in the `entitlements` field. This is Entitlement Mirroring. | Your app. | | **Entitlement grant** | What a user ends up with for one entitlement, and where it came from: a rule, an answer your app passed, or an override. | Derived by the engine. | Your app's own answer and the Playbook's can disagree. By default your app's answer wins, and the Playbook's rule is then only used to tell you which plans include the entitlement. That default is the Entitlement Mirroring precedence setting; set it the same way in the browser and on your server so they agree. It is separate from how overrides combine, below. **Caution — Planned, not shipped: AccountContext and overrides:** Two Runtime pieces are approved but not built yet, so do not write code against them: - **AccountContext**: account-level input that the SDK would merge with the UserContext. When both carry the same key, the UserContext value would win, with a warning if the values differ. The merge rules beyond that are still being designed. - **Entitlement overrides**: a plan, add-on or entitlement granted to one account or one user, optionally until an end date. Overrides only add access. Until they ship, supply account-level facts in the UserContext yourself. This page will describe them as shipped when they are. ## How initialization drives a decision 1. **Initialize.** You give the SDK the Playbook (a bundled file, or a public key so it can fetch the launched Release) and the user. See [Initialize the SDK](/revturbine-sdk/getting-started/initialization/). 2. **Load.** In hosted mode the SDK fetches the Playbook once and re-checks for a new Release about once a minute, keeping the last good copy if a refresh fails. Until the first Playbook arrives, a check **denies** with `config_unavailable`. It never grants on a guess. 3. **Change the context.** `identify()`, `update()` and a client-session refresh change the Runtime. The SDK re-evaluates against the Playbook it already holds. No Playbook is fetched for that. 4. **Decide.** `can()` returns an entitlement result; a `` returns the one placement that won, after eligibility, caps, cooldowns and priority. Local evaluation is what keeps this fast and independent of RevTurbine's availability. Two boundaries matter: - **Browser versus server.** The browser's decision drives the UI. It is a convenience, because a user controls their own browser. Your server makes the decision that protects money and data, by building its own instance from the Playbook and **its own** record of the user. See [Client vs Server Enforcement](/revturbine-sdk/concepts/enforcement/). - **Local versus hosted.** Hosted changes where the Playbook and the server-derived user state come from, not where the decision runs. See [Runtime Modes](/revturbine-sdk/guides/runtime-modes/). ## A worked example You sell a "batch export" feature, included in Pro, with a limit of 10 exports a month on Starter. - **Playbook:** the `batch_export` entitlement; one rule that gives Pro unlimited and Starter a limit of 10; a placement that shows an upgrade banner when a Starter user has used 8. - **Runtime:** Maya is on Starter and has used 8 (UserContext and usage). - **Decision:** `can('batch_export')` returns allowed, with 2 remaining. The banner placement is eligible, so the `` shows it. - **Change:** Maya upgrades. Stripe tells RevTurbine; the client session brings the new plan to the SDK; the Runtime changes, and the same Playbook now says unlimited. Nothing was redeployed. ## Where to go next - [Core Concepts](/revturbine-sdk/concepts/core-concepts/): entitlements, gates and placements. - [Entitlements](/revturbine-sdk/guides/entitlements/) and [Placements](/revturbine-sdk/guides/placements/): each primitive in depth. - [What Owns What](/revturbine-sdk/concepts/source-of-truth/): which system is authoritative for each fact. - [Initialize the SDK](/revturbine-sdk/getting-started/initialization/): the code. # What Owns What > The source-of-truth map across RevTurbine, your app, and Stripe — who owns plan definitions, prices, subscription status, feature-access policy, usage, checkout, and billing-critical enforcement. RevTurbine sits between your app and Stripe, and the single biggest question a technical evaluator has is *"where does each piece of truth actually live?"* Here's the map. ## The map | Concern | RevTurbine | Your app | Stripe | |---|---|---|---| | **Plan catalog** — which plans/tiers exist | ✅ **Authoritative** (the Playbook) | — | mirrors as products | | **Prices** | mirror (UI helper) | — | ✅ **Authoritative** | | **Subscription status** — active / plan / canceled | holds the server-derived plan and payment state (from Stripe webhooks) and delivers it to the SDK through the client session | may pass `plan_handle` as an optimistic override at the upgrade moment; supplies it always in local mode | ✅ **Authoritative** | | **Feature-access policy** — what each plan entitles | ✅ **Authoritative** (entitlement rules) | — | — | | **Usage counts** | evaluates them against limits | ✅ **reports** usage (`rt.update`) | — | | **Checkout & payment** | starts it (a CTA opens checkout) | hosts the flow | ✅ **Processes** payment | | **When to show a placement / upsell** | ✅ **Decides** (decision engine) | renders the `` | — | | **Billing-critical enforcement** — actually granting a paid action | advises (policy) | ✅ **Enforces** (server-side) | — | ## Reading the map - **RevTurbine owns *policy*, not money.** The plan catalog, what each plan entitles, and when a placement shows are all RevTurbine's — a PM edits them in the dashboard and releases, with no code deploy. RevTurbine never charges anyone. - **Stripe owns *money and subscription state*.** Prices, who is subscribed, and payment processing are Stripe's. RevTurbine mirrors prices into the Playbook as a **UI helper** for rendering — but Stripe is the price authority. With Stripe connected, subscription webhooks update the customer's plan and payment health on RevTurbine's side and the SDK receives them through the client session — see [Billing & Stripe](/revturbine-sdk/guides/billing/). Your app can pass `plan_handle` in user context as an optimistic override (at the upgrade moment, before the webhook lands); in local mode it supplies the plan always. - **Your app owns *enforcement and usage*.** You report usage, you host checkout, and — critically — **you enforce billing-critical actions on your server.** See [Client vs Server Enforcement](/revturbine-sdk/concepts/enforcement/). **Note — Why prices live in Stripe, not the Playbook:** The Playbook carries `price_amount` / `stripe_price_id` so the SDK can render a pricing card without a round-trip. That copy is denormalized for display only — Stripe remains the authority, and the mirror is kept fresh from Stripe. Never treat the Playbook's price as the number you charge. ## What this buys you Because your app only ever knows **slots and entitlements** — never plan names or prices hard-coded in code — cascading monetization changes stay out of your codebase. Launch a new plan, run a discount, restructure tiers: the catalog and policy change in RevTurbine, the numbers change in Stripe, and your app keeps rendering the same slots and checking the same entitlements. The first monetization strategy is never the last — and this split is what keeps your code from having to change every time it moves. ## Related - [Client vs Server Enforcement](/revturbine-sdk/concepts/enforcement/) — the policy-vs-enforcement split in detail - [Placements](/revturbine-sdk/guides/placements/) — slots, CTA actions, and the empty-state rule - [Entitlements](/revturbine-sdk/guides/entitlements/) — the entitlement rules RevTurbine owns # Recommended API Path > The small, blessed surface to reach for first — can() and gate() to check and protect, and to render, and the Slot / Gate / Playbook vocabulary. Everything else is Advanced. The SDK has a large surface, but most integrations only need a handful of calls. Reach for these first; everything else is **Advanced**. ## Two questions, two verbs Almost every monetization decision is one of two questions: - **"Can this user do X?"** → *check* it with `can()` (or the `useEntitlement` hook). - **"Only let them do X if they're entitled — and show an upsell if not."** → *protect* it with `gate()` (imperative) or `` (declarative React). And when you just want to render whatever the control plane decides for a spot in your UI, drop in a **``**. ## What to reach for | You want to… | Use | Kind | |---|---|---| | Check access imperatively | `rt.can(handle)` → `EntitlementResult` | verb | | Check access in React | `useEntitlement({ handle })` / `useCan(handle)` | hook | | Run an action only if entitled | `rt.gate(action, fn)` → `{ ran, entitlement }` | verb | | Gate a React subtree + show an upsell | `…` | component | | Render a control-plane placement | `` | component | | Track an event | `rt.track(name, data)` | verb | | Report usage | `rt.update({ usage })` | verb | ## `can()` vs `gate()` vs `` They sound alike but do different jobs: - **`can()`** *asks* — it returns the answer and does nothing else. ```ts const access = await rt.can('data_export'); if (!access.allowed) showUpgrade(); ``` It returns the rich `EntitlementResult` (`allowed`, `status`, `reason`, and — for limit-bearing entitlements — `limit` / `used` / `remaining`). `can()` is asynchronous by design, so custom resolvers can be injected. - **`gate(action, fn)`** *asks and acts* — it runs `fn` only when the user is entitled, and hands back the entitlement so you can open a paywall otherwise. ```ts import { useRevTurbine } from '@revturbine/sdk'; const { sdk: rt } = useRevTurbine(); // the browser SDK; `gate()` lives here, not on a headless session const gated = await rt.gate('export_pdf', () => exportPdf()); if (!gated.ran) openPaywall(gated.entitlement); ``` - **``** is the declarative React version — it renders its children when access is granted and the resolved upsell placement when it isn't. ```tsx ``` **Caution — The client check is convenient, not authoritative:** `can()`, `gate()`, and `` run in the browser, where a determined user can tamper with the result. For **billing-critical or abuse-sensitive** actions, treat the client check as a UX convenience and **re-run the same entitlement check on your server** (or via a server SDK) before you actually grant access. See [Client vs Server Enforcement](/revturbine-sdk/concepts/enforcement/) for the authoritative placement, entitlement, and outage behavior; your server owns the money-safe decision. ## The vocabulary: Slot · Gate · Playbook Three names cover the customer-facing surface: - **``** — the general placement slot. It renders whatever placement the control plane resolves for a spot (banner, modal, toast, meter, …). ```tsx ``` - **``** — the entitlement gate (above): render children when entitled, an upsell when not. - **`Playbook`** — the portable monetization config the SDK evaluates against: plans, entitlements, segments, and placements. In local mode you load it (typically `revturbine.playbook.json`) at init; in live mode the SDK fetches it. Each has an older name that stays exported — prefer the canonical one: | Canonical | Also exported as | What it is | |---|---|---| | `` | `SurfaceSlotComponent`, `RTSlot` | general placement slot component | | `` | `AccessGateSurfaceSlot` | entitlement-gate component | | `Playbook` | `ExportedConfig`, `RevTurbineConfig` | the portable monetization config; both aliases are deprecated | ## Everything else is Advanced Provider architecture, custom slot types, headless controllers (`usePlacement` and the imperative controllers), theming internals, and the generated object detail are all still available — they're just not the first thing you reach for. See the **Guides** and the **API Reference** when you need them. ## Related - [Quickstart](/revturbine-sdk/) — gate one action end to end - [Entitlements](/revturbine-sdk/guides/entitlements/) — the full entitlement model behind `can()` / `gate()` - [Placements](/revturbine-sdk/guides/placements/) — what `` renders and how decisions resolve # Initialize the SDK > One canonical initialization example per runtime — React and headless in the browser, and your Node server — in local and hosted mode, with the option names the SDK actually reads. Every other page links here for initialization, so these are the only copy-paste starting points. Each one compiles against the published SDK in CI. Which entry point you use depends on **where the code runs**: | Where it runs | Entry point | Local mode | Hosted mode | |---|---|---|---| | Browser, React | `RevTurbineProvider` from `@revturbine/sdk` | `localRuntime: { playbook }` | `publicKey` + `clientSession` | | Browser, no React | `initRevTurbine` from `@revturbine/sdk/headless` | `localRuntime: { playbook }` | `publicKey` + `clientSession` | | Your Node server | `initRevTurbine` from `@revturbine/sdk/headless`, plus `RevTurbineServer` | the Playbook snapshot you loaded | server key for Playbook delivery and events; `RevTurbineServer` for client sessions | The option names are fixed, and they are not interchangeable: | Option | Where | What it is | |---|---|---| | `publicKey` | the browser | Your ingest key (`rtk_…`, type `public`). Safe to ship in a bundle. | | `apiKey` | headless server initialization and `RevTurbineServer` | Your server key (`rtk_…`, type `server`). Secret. Never in a browser bundle, never a `NEXT_PUBLIC_` variable. | | `localRuntime.playbook` | anywhere | The Playbook you load yourself (`revturbine.playbook.json`). | | `clientSession` | the browser | A function that returns a short-lived session token from **your** server. See [Client sessions](/revturbine-sdk/guides/client-sessions/). | In a browser, initialize with `publicKey`. On a trusted backend, headless `RuntimeMode.Server` accepts `apiKey` for Playbook loading and event ingest. The key never makes a hosted entitlement decision: evaluation runs locally with the user's context and the loaded Playbook. ## Before you go hosted 1. **Launch a Playbook.** Hosted mode serves your launched Playbook: `revturbine launch ./revturbine.playbook.json`. 2. **Mint an ingest key and list your origins.** In the dashboard, open **Settings → API Token Management → Ingest Keys** and mint a key. Add every origin that will load the SDK to its **Allowed origins**, including your local dev origin (for example `http://localhost:3000`). The list is fail-closed: a request from any other origin gets `403 origin_not_allowed`, and every check then denies with `config_unavailable`. 3. **Mint a server key.** Under **Settings → API Token Management → Server Token**. It is shown once. Put it in your server's environment as `REVTURBINE_API_KEY`, next to `REVTURBINE_TENANT_ID`. The tenant id must be the tenant the key belongs to. 4. **Add the client-session route** on your server, so the SDK can pick up plan, trial and payment state from Stripe. See [Client sessions](/revturbine-sdk/guides/client-sessions/). Local mode needs none of this. It is production-capable on its own, and it is the right first step: see [Runtime Modes](/revturbine-sdk/guides/runtime-modes/). ## Browser, React **Local**: the Playbook ships with your app. ```tsx title="src/App.tsx" import { RevTurbineProvider } from '@revturbine/sdk'; import { useMemo } from 'react'; import playbook from './revturbine.playbook.json'; export default function App() { const options = useMemo(() => ({ localRuntime: { playbook }, user: { id: 'user_123', plan_handle: 'starter' }, }), []); return ( ); } ``` **Hosted**: the SDK fetches your launched Playbook with the public key, and the `clientSession` callback brings in the plan Stripe says the user is on. ```tsx title="src/revturbine-provider.tsx" import { RevTurbineProvider } from '@revturbine/sdk'; import { useMemo, type ReactNode } from 'react'; import { fetchClientSession } from './revturbine-client-session'; export function RevTurbine({ userId, children }: { userId: string; children: ReactNode }) { const options = useMemo( () => ({ publicKey: process.env.NEXT_PUBLIC_REVTURBINE_PUBLIC_KEY!, user: { id: userId }, clientSession: fetchClientSession, }), [userId], ); return {children}; } ``` `user` carries only the id: the plan arrives from Stripe through the client session. `tenantId` is optional, because the key names its tenant. `endpoint` is optional and defaults to RevTurbine's hosted control plane. `fetchClientSession` is the browser half of the [client-session flow](/revturbine-sdk/guides/client-sessions/#the-browser-callback). The provider needs a stable `options` object, so build it with `useMemo` as above; a new object on every render re-initializes the SDK. ## Browser, headless For Vue, Svelte, Angular or plain TypeScript. Same options, no React. ```ts title="src/revturbine.ts" import { initRevTurbine } from '@revturbine/sdk/headless'; import { fetchClientSession } from './revturbine-client-session'; export async function startRevTurbine(userId: string) { return initRevTurbine({ publicKey: process.env.NEXT_PUBLIC_REVTURBINE_PUBLIC_KEY!, user: { id: userId }, clientSession: fetchClientSession, }); } ``` `initRevTurbine` is asynchronous and resolves to a session. Use it directly (`await session.can('export')`), or reach the core SDK as `session.sdk`. For local mode, swap the two hosted options for `localRuntime: { playbook }`. ## Your Node server A server has two jobs, and they use two different objects. **Decide** whether a user may do something, before you do the paid work. The headless SDK evaluates your Playbook in-process, for one user per instance. Load the Playbook once per process and build a request-scoped instance: ```ts title="src/server/revturbine.ts" import { initRevTurbine, RuntimeMode, type RevTurbineUserContext, } from '@revturbine/sdk/headless'; import playbook from './revturbine.playbook.json'; /** One instance per request: the SDK holds a single user context per instance. */ export function revturbineFor(user: RevTurbineUserContext) { return initRevTurbine({ runtimeMode: RuntimeMode.LocalOnly, localRuntime: { playbook }, user, }); } ``` Get the Playbook with `revturbine download --live --save ./revturbine.playbook.json` in your build or deploy step, so the server evaluates the Playbook you last launched. A launch reaches a server when it next loads that file, so restart or redeploy after a launch. The full local and connected patterns are on [Server-Side Integration](/revturbine-sdk/getting-started/server-side/). **Mint** client sessions and write server-only user state with your server key. `RevTurbineServer` is one long-lived object per process, not one per request: ```ts title="src/server/revturbine-server.ts" import { RevTurbineServer } from '@revturbine/sdk/headless'; export const revturbineServer = new RevTurbineServer({ tenantId: process.env.REVTURBINE_TENANT_ID!, apiKey: process.env.REVTURBINE_API_KEY!, endpoint: process.env.REVTURBINE_ENDPOINT ?? 'https://revturbine.com/app', }); ``` `RevTurbineServer` does not evaluate anything. What it can do: mint client sessions (`clientSessions.create`), assign a seat type (`assignSeatType`), write built-in dimensions (`setBuiltinDimensions`) and read them back (`getBuiltinDimensions`). The server key stays in server code. **Caution — Keep the server key out of the browser:** RevTurbine refuses a server key presented from a browser, and a key in a bundle is a leaked key. If it ever reaches the client, rotate it under **Settings → API Token Management → Server Token**. ### Connected backend For hosted Playbook updates, use the server runtime after authenticating the caller and constructing their context from trusted application data: ```ts import { initRevTurbine, RuntimeMode, type RevTurbineUserContext } from '@revturbine/sdk/headless'; export async function canExport(user: RevTurbineUserContext): Promise { const session = await initRevTurbine({ runtimeMode: RuntimeMode.Server, tenantId: process.env.REVTURBINE_TENANT_ID!, apiKey: process.env.REVTURBINE_API_KEY!, user, analytics: false, anonymousTelemetry: false, }); try { if (session.sdk.getPlaybookLoadFailure()) return false; return (await session.can('data_export')).allowed; } finally { session.sdk.dispose(); } } ``` Keep an identified session scoped to that user and operation. Share immutable Playbook snapshots, not sessions, between callers. A connected instance treats `localRuntime.playbook` as a startup seed and refreshes it from hosted delivery; `LocalOnly` continues using the supplied snapshot without fetching. The [cloud integration tutorial](/revturbine-sdk/tutorials/vercel-edge-entitlements/) adds authenticated context tokens, an explicitly bounded shared Playbook cache, and provider adapters. ## Next - [Client sessions](/revturbine-sdk/guides/client-sessions/): the full round trip, with the server route and the browser callback. - [Server-Side Integration](/revturbine-sdk/getting-started/server-side/): re-check before paid work, and what a server can and cannot report. - [Configuration reference](/revturbine-sdk/reference/configuration/): every option. # Installation > Install the RevTurbine SDK and configure your project. ## Web SDK (React) ### Install `@revturbine/sdk` is published to the public npm registry — no auth, token, or registry configuration required. **npm** ```bash npm install @revturbine/sdk ``` **pnpm** ```bash pnpm add @revturbine/sdk ``` **yarn** ```bash yarn add @revturbine/sdk ``` ### Peer Dependencies The web SDK requires: | Package | Version | |---|---| | `react` | `>=18.0.0` | | `react-dom` | `>=18.0.0` | ### TypeScript The SDK ships with full TypeScript declarations. No additional `@types/` packages are needed. Recommended `tsconfig.json` settings: ```json { "compilerOptions": { "strict": true, "moduleResolution": "bundler", "esModuleInterop": true, "jsx": "react-jsx" } } ``` ## Headless / Non-React The same `@revturbine/sdk` package includes a headless entry point for use without React: ```ts import { initRevTurbine } from '@revturbine/sdk/headless'; ``` This import tree-shakes React dependencies. See the [Headless API guide](/revturbine-sdk/guides/headless-api/) for usage patterns with vanilla JS, Vue, Svelte, and Angular. ## Server-Side Server SDKs are available for backend entitlement checks and placement resolution: | Language | Package | |---|---| | Node.js | `@revturbine/sdk` (headless entry) | | Python | `revturbine` (PyPI) | :::note Both server SDKs are published — `@revturbine/sdk` (npm) and `revturbine` (PyPI) — and produce byte-identical decisions for the same config and user context. ::: ## CLI The **`@revturbine/cli`** package validates and launches Playbooks from the terminal. Install it as an exact development dependency so its bundled schema snapshot is pinned with your repository: ```bash npm install -D --save-exact @revturbine/cli npx revturbine --help ``` Or run it without installing: ```bash npx @revturbine/cli validate ./revturbine.playbook.json ``` ### Authenticate The CLI signs in with a browser device-flow login — there are no API keys or environment variables to set. Authorize it once per machine: ```bash revturbine login ``` This opens your browser to approve the device, then stores a tenant-scoped token at `~/.revturbine/credentials.json` (file mode `0600`). Run `revturbine status` to confirm you're connected — or `revturbine signup` to create an account from the terminal first. Commands that only read or validate a local file, like `validate`, need no login. See the [CLI reference](/revturbine-sdk/reference/cli/) for the full command surface, file-selector rules, generated handle types, and the draft → Release lifecycle. ## Supported Runtimes | Runtime | Minimum Version | |---|---| | Node.js | 22.13+ | | Modern browsers | Chrome 90+, Firefox 90+, Safari 15+, Edge 90+ | | React | 18.0+ | | TypeScript | 5.0+ | ## Next Steps - [Quickstart](/revturbine-sdk/) — render your first placement - [Runtime modes](/revturbine-sdk/guides/runtime-modes/) — choose server, custom endpoints, or local-only # Python SDK > Headless, in-memory entitlement and placement decisioning for Python services — RevTurbineCustomerSdk. The Python SDK is a **headless *server* library**: it decides entitlements and placements **in-process**, with no network call and no persistence beyond memory. The server holds the user context and a `Playbook` snapshot and passes both to the SDK; the SDK decides locally. It is intentionally a small surface — the same decision core as the TypeScript SDK's headless `LocalRuntime`, exposed as a clean public class. It is **not** a port of the browser SDK (no React, no hooks, no decision cache — see [Non-goals](#non-goals)). **Note:** The Python decision output is **byte-identical** to the canonical TypeScript `LocalRuntime` for the same inputs. A cross-language parity corpus (`tests/parity/`) drives TypeScript, Python, and Rust through the same fixtures and fails CI on any divergence — the Python port is never allowed to drift. ## Install The `revturbine` package (requires Python ≥ 3.10) is published to **PyPI**. Install it in a virtual environment: **macOS / Linux** ```bash python3 -m venv .venv .venv/bin/python -m pip install revturbine ``` **Windows PowerShell** ```powershell py -m venv .venv .venv\Scripts\python.exe -m pip install revturbine ``` Use that environment's Python to run your app too. A virtual environment also handles PEP 668's “externally managed environment” restriction without modifying system Python. Pin to a version whose schema matches the `Playbook` you deploy — see [Refreshing the Playbook](#refreshing-the-playbook): ```bash python -m pip install "revturbine==" ``` Here `python` means your virtual environment's interpreter, as in the commands above. ## Quick start Construct one SDK per `(user_context, exported_config)`, then check entitlements and decide placements: ```python import json from revturbine import RevTurbineCustomerSdk, UserContext with open("revturbine.playbook.json") as f: exported_config = json.load(f) user_context: UserContext = { "tenant_id": "tenant_abc", "user_id": "user_123", # Optional — the user's current plan and usage: "plan_handle": "pro", "usage": {"api_calls": {"used": 900.0, "limit": 1000.0}}, } sdk = RevTurbineCustomerSdk( user_context=user_context, exported_config=exported_config, ) # Entitlement gate result = sdk.can("advanced_analytics") if not result["allowed"]: raise PermissionError(result["reason"]) # Placement decision (which payload, if any, to show) decision = sdk.get_placement_decision( {"placement_id": "pl_dashboard_upsell", "user_id": "user_123"} ) if decision["visible"]: render(decision["output"]) # Batch form — same decision path, order-preserving decisions = sdk.get_placement_decisions( [ {"placement_id": "pl_a", "user_id": "user_123"}, {"placement_id": "pl_b", "user_id": "user_123"}, ] ) ``` ### User context | Key | Required | Meaning | |---|---|---| | `tenant_id` | ✅ | Tenant identifier. | | `user_id` | ✅ | Current user identifier. | | `plan_handle` | — | The user's current plan handle (feeds the plan + entitlements providers). | | `plan_name` | — | Display name; defaults to `plan_handle`. | | `usage` | — | Per-entitlement overrides: `{handle: {"used": float, "limit": float}}`. | | `trial_status` | — | Already-derived trial state, overlaid onto the plan provider. | | `payment_failed` | — | Hard-failure billing-recovery signal. | | `payment_at_risk` | — | Soft billing-attention signal. | | `tiers` | — | Current tier handle per `capability_tier` entitlement handle. | | `segment_ids` | — | Tenant segment handles your app already resolved for this user. They count as segment membership for chipped payloads, segment-targeted entitlement rules, and the eligible plan and add-on helpers. | | `custom` | — | Customer traits that segment predicates evaluate over, e.g. `{"sessions": 12}`. | | `builtin_dimensions` | — | Built-in segment dimension values your server sets, e.g. `{"subscription_state": "paid"}`. This is the only way a built-in `rt..` segment can match on a server port. | | `experiments` | — | Experiment-handle to variant-handle assignments, for experiment segments. | `tenant_id` and `user_id` are the only required fields; an empty/missing one raises `ValueError` at construction. A user is in a segment only if `segment_ids` names it or its predicates match `custom`, the plan or `builtin_dimensions`. Anything else fails closed: a payload or rule chipped to a segment the SDK can't confirm is not served. Since BL-0369 the SDK no longer treats every configured segment as matched. `plan_handle` must equal the plan's `unique_handle` in the Playbook. Usage keys must equal entitlement handles, and `used` is the current absolute balance, not an increment. **Caution — This is the compact server facade:** Import `UserContext` from `revturbine`, as shown above. It is a `TypedDict` for the local evaluator constructor—not the generated persisted model at `revturbine.types.UserContext`, and not TypeScript's broader `RevTurbineUserContext`. It intentionally has no `id`, `plan`, `trial`, `custom`, `entitlements`, `experiments`, or `instances` fields. Map those app values into the supported fields before construction. See the [cross-language context mapping](/revturbine-sdk/user-context/#server-sdk-context-shapes). ### Public surface | Method | Returns | |---|---| | `can(handle, context=None)` | `{status, allowed, reason, ...}`; preferred entitlement re-check | | `check_entitlement(handle, context=None)` | `{status, allowed, reason, ...}` | | `get_placement_decision(input)` | `{placement_id, visible, output, content, ...}` | | `get_placement_decisions(inputs)` | ordered `list` of the above | `can()` is an alias of `check_entitlement()`. There is **no storage/persistence parameter** — the instance is stateless and in-memory by construction; construct a fresh one per user context. ## Runtime model Unlike the TypeScript SDK (which has `revturbine_server` / `custom_endpoints` / `local_only` runtime modes), the Python SDK is **local-only by design**. There is no runtime-mode switch and no network path: | | Python `RevTurbineCustomerSdk` | |---|---| | Network | None — decides from the supplied `Playbook` | | Persistence | None — `InMemoryStorage` only; no file/db, no decision cache | | State | None across calls; no interaction/suppression hydration | | Inputs | `user_context` + `Playbook`, both supplied by the server | This makes it safe on a request hot path: an entitlement gate is a pure in-process computation, with no decision-API round-trip and no shared mutable state between requests. ## Refreshing the Playbook The `Playbook` is a snapshot exported from the RevTurbine control plane. The SDK never fetches or caches it — your service owns its lifecycle: 1. Fetch/export the latest `Playbook` JSON (on boot, on a timer, or on a webhook from the control plane). 2. Atomically replace the shared **Playbook snapshot**. Construct a new `RevTurbineCustomerSdk` for each request with that request's authenticated user context. Never share a user-bound SDK instance across requests. ```python # Replace this snapshot on refresh, rather than modifying it in place. playbook = load_config() def check_request(user_context, entitlement): sdk = RevTurbineCustomerSdk( user_context=user_context, exported_config=playbook, ) return sdk.check_entitlement(entitlement) ``` Pin the installed SDK tag to a version whose schema matches the `Playbook` you deploy; a snapshot newer than the SDK's schema can decide differently than your TypeScript frontend. ## What is ported Segment evaluation and targeting-state derivation from a raw user context are ported and parity-locked, so a Python gate and a TypeScript render agree given the same user context — not only the same pre-resolved traits. `segment_ids` remains available as an explicit override. ## Non-goals The Python SDK is the headless server decision core only. The following are **intentionally not ported** — they are browser/full-SDK concerns with no server equivalent: - React / JSX components and hooks (no Python equivalent). - Browser storage, `localStorage` theme persistence, `window.RevTurbine`. - HTTP-backed / dual-mode dispatch (`runtime_mode`) — the Python library is local-only. - `identify`, `dismiss` / `snooze` / `convert`, `track_treatment_interaction`, `get_trial_status`, `capture`, `bootstrap_placement_decisions`, `get_user_context`, decision-cache / interaction-state hydration. **Caution:** The package also contains an older thin-RPC client (`revturbine_server`). It is unrelated to `RevTurbineCustomerSdk`; use `RevTurbineCustomerSdk` for all new server integrations. ## Parity guarantee Every public method delegates, with zero added decision logic, to the same decision substrate the TypeScript SDK uses. Our parity suite runs all three ports — TypeScript, Python, and [Rust](/revturbine-sdk/getting-started/rust/) — through identical fixtures and asserts **byte-identical** normalized output; a divergence is treated as a Python-port bug, never a fixture to loosen. This is what lets you gate in Python and render in TypeScript and trust they decided the same thing. # React Integration > Use the RevTurbine SDK with React — Provider, hooks, and components. The React integration provides a provider, hooks, and drop-in components for placement rendering and entitlement checks. ## RevTurbineProvider Wrap your app (or a subtree) with the provider: ```tsx import { RevTurbineProvider, RuntimeMode } from '@revturbine/sdk'; import playbook from './revturbine.playbook.json'; import { useMemo } from 'react'; export default function App() { const options = useMemo(() => ({ runtimeMode: RuntimeMode.LocalOnly, // evaluate the bundled Playbook locally localRuntime: { playbook }, user: { id: 'user_123', plan_handle: 'starter', }, }), []); return ( ); } ``` ### Props | Prop | Type | Description | |---|---|---| | `options` | `RevTurbineInitOptions` | SDK initialization options (see [Configuration Reference](/revturbine-sdk/reference/configuration/)) | | `bootstrapPlacements` | `BootstrapPlacementInput[]` | Placements to preload on mount | | `colorScheme` | `'light' \| 'dark' \| 'system'` | Placement palette; defaults to `'system'` and follows the device preference. Changing this prop does not reinitialize the SDK. | | `children` | `ReactNode` | Your app | Pass `colorScheme` directly to the provider, outside `options`. See [Theming](/revturbine-sdk/guides/theming/) for branding overrides and palette selection. ### Bootstrap Placements Preload placement decisions during initialization to eliminate loading flicker: ```tsx ``` ## Hooks ### useRevTurbine Access the SDK instance and provider state: ```tsx import { useRevTurbine } from '@revturbine/sdk'; function StatusBar() { const { sdk, isReady, initStatus, colorScheme } = useRevTurbine(); if (!initStatus.ok) { return (

SDK initialization failed during {initStatus.phase}: {initStatus.message}

{initStatus.remediation}

); } if (!isReady || !sdk) return Loading SDK…; return SDK ready · {colorScheme} theme; } ``` **Returns:** | Field | Type | Description | |---|---|---| | `sdk` | `RevTurbineCustomerSdk \| null` | SDK instance (null until ready) | | `isReady` | `boolean` | Whether the SDK has initialized | | `error` | `string` | Error message if initialization failed | | `initStatus` | `RevTurbineInitStatus` | Initialization outcome, available even when `sdk` is `null`: `{ ok, phase?, message?, remediation? }` | | `colorScheme` | `'light' \| 'dark'` | Resolved placement palette; `'system'` has already been resolved. Set the provider prop to change it. | | `setContext` | `(ctx: RevTurbineUserContext) => void` | Update user context dynamically | Read `initStatus` before treating a missing SDK as loading. On failure, `ok` is `false` and the phase, message and remediation describe what to fix. During initialization, `ok` is still `true`; wait for `isReady` and a non-null `sdk` before making checks. Mount this diagnostic inside your app's actual provider so it tests the options and Playbook your app uses. These fields are available from SDK 0.8.0. Development failure banners and theme warnings work in published packages from 0.8.9; `initStatus` remains available in production and in the earlier 0.8.x packages. See [Troubleshooting](/revturbine-sdk/guides/troubleshooting/) when initialization succeeds but a placement does not render. ### usePlacement Resolve a placement for a surface slot: ```tsx import { usePlacement } from '@revturbine/sdk'; function UpgradeBanner() { const { visible, content, isLoading, dismiss, ctaClick, } = usePlacement({ surfaceSlot: { id: 'dashboard_banner', surfaceTemplateIds: ['banner_placement'] }, }); if (!visible) return null; return (

{content?.header}

{content?.body}

); } ``` **Options:** | Field | Type | Default | Description | |---|---|---|---| | `surfaceSlot` | `RevTurbineSurfaceSlotConfig` | — | Slot configuration (id, surfaceTemplateIds) | | `userId` | `string` | — | Override user ID | | `contextMode` | `'auto' \| 'override'` | `'auto'` | Context resolution mode | | `traits` | `Record` | — | Additional targeting traits | | `ttlMs` | `number` | `300000` | Cache TTL in milliseconds | | `autoLoad` | `boolean` | `true` | Load placement on mount | **Returns:** | Field | Type | Description | |---|---|---| | `isLoading` | `boolean` | Whether the placement is being resolved | | `error` | `string` | Error message if resolution failed | | `visible` | `boolean` | Whether the placement should be shown | | `decision` | `RevTurbinePlacementDecision \| null` | Full decision object | | `content` | `object \| null` | Resolved content (header, body, cta_label, etc.) | | `refresh` | `() => Promise` | Re-resolve the placement | | `dismiss` | `(cooldownMs?) => Promise` | Dismiss and suppress | | `snooze` | `(seconds?) => Promise` | Temporarily suppress | | `remindMeLater` | `(seconds?) => Promise` | Alias for snooze | | `ctaClick` | `(target?) => Promise` | Record CTA click | | `ctaComplete` | `(target?) => Promise` | Record CTA completion | ### useEntitlement Check whether a user has access to a feature: ```tsx import { useEntitlement } from '@revturbine/sdk'; function ExportButton() { const { allowed, denied, gatedPlacement, recheck } = useEntitlement({ handle: 'data_export', autoGate: true, // auto-resolve upgrade placement if denied }); if (denied && gatedPlacement) { return ; } return ; } ``` **Options:** | Field | Type | Default | Description | |---|---|---|---| | `handle` | `string` | — | Entitlement handle (required) | | `autoCheck` | `boolean` | `true` | Check on mount | | `autoGate` | `boolean` | `false` | Auto-resolve gated placement if denied | | `gatePlacementRequest` | `object` | — | Placement config for the gate | **Returns:** | Field | Type | Description | |---|---|---| | `isLoading` | `boolean` | Whether the check is in progress | | `error` | `string \| null` | Error message | | `result` | `EntitlementResult \| null` | Full result | | `allowed` | `boolean` | Access granted | | `limited` | `boolean` | Approaching limit | | `denied` | `boolean` | Access denied | | `gatedPlacement` | `PlacementOutput \| null` | Upgrade placement (if `autoGate: true`) | | `recheck` | `() => Promise` | Re-check entitlement | ### Keeping mounted gates current `Gate` and `useEntitlement` subscribe to SDK context and usage notifications. After `rt.update({ usage: { api_calls: total } })` or a supported context update, the mounted gate rechecks; a forced remount or page reload is not required. `recheck()` remains available for an explicit retry. This subscription does not poll your billing database. When a subscription or balance changes elsewhere, fetch fresh authoritative context and pass it to the SDK. Updating React state that the SDK never receives cannot change its decision. Preserve the provider's stable options object and update facts through the SDK rather than rebuilding the provider for every balance change. Verify a gate stays mounted while usage crosses its limit and then resets. Also verify a paid-to-free user switch clears the old grants. See [identity lifecycle](/revturbine-sdk/guides/identity-lifecycle/) and [usage periods](/revturbine-sdk/guides/usage-periods/). ### useUsageSnapshot Get current usage balances: ```tsx import { useUsageSnapshot } from '@revturbine/sdk'; function UsagePanel() { const { usage, refresh } = useUsageSnapshot(); return (

API calls: {usage.api_calls?.current} / {usage.api_calls?.limit}

); } ``` ### useRevTurbineTheme Access the resolved theme: ```tsx import { useRevTurbineTheme } from '@revturbine/sdk'; function ThemedCard({ children }) { const theme = useRevTurbineTheme(); return (
{children}
); } ``` ## Drop-In Components For the simplest integration, use `` — it resolves the placement and renders the matching built-in slot: ```tsx import { Slot } from '@revturbine/sdk'; export function Surfaces() { return ( <> {/* Banner */} {/* Modal gate */} {/* Inline card */} ); } ``` See the [Component Gallery](/revturbine-sdk/components/) for interactive demos of every built-in slot component. ## Dynamic Context Updates Update the user context after login, plan change, or navigation: ```tsx const { setContext } = useRevTurbine(); // After login setContext({ id: user.id, plan_handle: user.planHandle, custom: { role: user.role }, }); ``` All active placements and entitlement checks re-evaluate automatically when context changes. ## Next Steps - [Entitlements Guide](/revturbine-sdk/guides/entitlements/) — feature gates, usage limits, and credits - [Placements Guide](/revturbine-sdk/guides/placements/) — slot-based and entitlement-based patterns - [Theming Guide](/revturbine-sdk/guides/theming/) — customize slot component appearance - [Interactive Playground](/revturbine-sdk/playground/) — try live editable SDK scenarios # Rust SDK > Headless, in-memory entitlement and placement decisioning for Rust services — RevTurbineCustomerSdk. The Rust SDK is a **headless *server* library**: it decides entitlements and placements **in-process**, with no network call and no persistence beyond memory. Your service holds the user context and a `Playbook` snapshot and passes both to the SDK; the SDK decides locally. It is the same decision core as the TypeScript SDK's headless `LocalRuntime` and the [Python SDK](/revturbine-sdk/getting-started/python/), exposed as a small public type. It is **not** a port of the browser SDK (no components, no hooks, no decision cache — see [Non-goals](#non-goals)). **Note:** The Rust decision output is **byte-identical** to the canonical TypeScript `LocalRuntime` for the same inputs. A cross-language parity corpus (`tests/parity/`) drives TypeScript, Python, and Rust through the same fixtures and fails CI on any divergence — the Rust port is never allowed to drift. The decision core is **synchronous**. It performs no I/O, so there is no async runtime dependency and no `.await` on the public surface — an entitlement gate is a plain function call, safe on a request hot path. ## Install The `revturbine` crate is published to **crates.io**: ```toml [dependencies] revturbine = "0.2" ``` The minimum supported Rust version is **1.88**, declared as `rust-version` in the crate manifest. Pin to a version whose schema matches the `Playbook` you deploy — see [Refreshing the Playbook](#refreshing-the-playbook). ## Quick start Construct one SDK per `(user_context, playbook)`, then check entitlements and decide placements: ```rust use revturbine::runtime::PlacementDecisionInput; use revturbine::sdk::{RevTurbineCustomerSdk, UserContext}; use serde_json::json; let playbook: serde_json::Value = serde_json::from_str(&std::fs::read_to_string("revturbine.playbook.json")?)?; let user = UserContext { tenant_id: "tenant_abc".to_string(), user_id: "user_123".to_string(), // Optional — the user's current plan and usage: plan_handle: Some("pro".to_string()), usage: Some(json!({ "api_calls": { "used": 900, "limit": 1000 } })), ..Default::default() }; let mut sdk = RevTurbineCustomerSdk::new(&user, &playbook)?; // Entitlement gate let check = sdk.check_entitlement("advanced_analytics", None); if !check.allowed { return Err(check.reason.unwrap_or_else(|| "denied".to_string()).into()); } // Placement decision (which payload, if any, to show) let decision = sdk.get_placement_decision(&PlacementDecisionInput { placement_id: "pl_dashboard_upsell".to_string(), user_id: "user_123".to_string(), }); if decision["visible"] == true { // Render `decision["output"]` with your own UI layer. println!("{}", decision["output"]); } // Batch form — same decision path, order-preserving let decisions = sdk.get_placement_decisions(&[ PlacementDecisionInput { placement_id: "pl_a".into(), user_id: "user_123".into() }, PlacementDecisionInput { placement_id: "pl_b".into(), user_id: "user_123".into() }, ]); ``` **Caution:** Read **`allowed`**, not `status`. The `degrade` enforcement mode returns `status: "limited"` *and* `allowed: true` — gating on `status == "allowed"` would wrongly block a user who is meant to be served a degraded experience. ### User context | Field | Required | Meaning | |---|---|---| | `tenant_id` | ✅ | Tenant identifier. | | `user_id` | ✅ | Current user identifier. | | `plan_handle` | — | The user's current plan handle (feeds the plan + entitlements providers). | | `plan_name` | — | Display name; defaults to `plan_handle`. | | `usage` | — | Per-entitlement overrides: `{handle: {used, limit}}`. | | `trial_status` | — | Already-derived trial state, overlaid onto the plan provider. | | `payment_failed` | — | Billing-recovery signal for the retention qualifiers. | | `payment_at_risk` | — | Billing-recovery signal for the retention qualifiers. | | `tiers` | — | Current tier per `capability_tier` entitlement, for the tier gate. | `UserContext` implements `Default`, so `..Default::default()` covers every field you do not set. Only `tenant_id` and `user_id` are required; construction rejects either one when it is empty. `plan_handle` must equal the plan's `unique_handle` in the Playbook. Usage keys must equal entitlement handles, and `used` is the current absolute balance, not an increment. **Caution — This is the compact server facade:** Import `UserContext` from `revturbine::sdk`, as shown above. Do not substitute the generated persisted model at `revturbine::types::UserContext`, and do not copy TypeScript's broader `RevTurbineUserContext` shape. The local evaluator accepts `plan_handle` plus optional `plan_name` rather than a `plan` object, and `trial_status` rather than `trial`; it does not accept raw `custom`, `entitlements`, `experiments`, or `instances`. See the [cross-language context mapping](/revturbine-sdk/user-context/#server-sdk-context-shapes). Omitting `trial_status` is the one easy mistake: without it every `trial_*` gate reads "no trial" and silently declines rather than erroring. Supply it whenever you run trial-triggered placements. ### Public surface | Method | Returns | |---|---| | `check_entitlement(handle, context)` | `EntitlementCheckResult` | | `get_placement_decision(input)` | `serde_json::Value` | | `get_placement_decisions(inputs)` | `Vec`, order preserved | | `get_placement(config)` | `Option` — surface-keyed slot resolution | That is the entire public API. There is **no storage or persistence parameter** — the instance is stateless and in-memory by construction; build a fresh one per user context. `check_entitlement` takes `&self`; the placement methods take `&mut self` because within one instance they track which placements have already been decided, which is what supersession reads. **Caution:** A malformed `Playbook` is an **error at construction**, not a degraded decision: `RevTurbineCustomerSdk::new` returns `Err`. A partially-understood Playbook can silently over-grant, so construction refuses it rather than guessing. ## Runtime model Unlike the TypeScript SDK (which has `revturbine_server` / `custom_endpoints` / `local_only` runtime modes), the Rust SDK is **local-only by design**. There is no runtime-mode switch and no network path: | | Rust `RevTurbineCustomerSdk` | |---|---| | Network | None — decides from the supplied `Playbook` | | Persistence | None — in-memory only; no file/db, no decision cache | | State | None across instances; no interaction/suppression hydration | | Concurrency | Synchronous; no async runtime dependency | | Inputs | `UserContext` + `Playbook`, both supplied by your service | ## Refreshing the Playbook The `Playbook` is a snapshot exported from the RevTurbine control plane. The SDK never fetches or caches it — your service owns its lifecycle: 1. Fetch/export the latest `Playbook` JSON (on boot, on a timer, or on a webhook from the control plane). 2. Parse it **once** and share the `serde_json::Value`; construction borrows the snapshot rather than taking ownership, so one parse serves every request. 3. Build a **new** `RevTurbineCustomerSdk` per user context from that shared snapshot. Because the type is stateless, "refresh" is just "re-parse and swap the shared snapshot" — e.g. behind an `ArcSwap` or an `RwLock`. Pin the crate to a version whose schema matches the `Playbook` you deploy; a snapshot newer than the crate's schema can decide differently than your TypeScript frontend. ## What is ported Segment evaluation and targeting-state derivation from a raw user context are ported and parity-locked, so a Rust gate and a TypeScript render agree given the same user context — not only the same pre-resolved traits. ## Non-goals The Rust SDK is the headless server decision core only. The following are **intentionally not ported** — they are browser/full-SDK concerns with no server equivalent: - UI components, hooks, and theming (no Rust equivalent). - Browser storage, `localStorage` theme persistence, `window.RevTurbine`. - HTTP-backed / dual-mode dispatch — the Rust crate is local-only. - `identify`, `dismiss` / `snooze` / `convert`, treatment-interaction tracking, `capture`, `bootstrap_placement_decisions`, decision-cache and interaction-state hydration. ## Parity guarantee Every public method delegates, with zero added decision logic, to the same decision substrate the TypeScript SDK uses. Our parity suite runs all three languages through identical fixtures and asserts **byte-identical** normalized output; a divergence is treated as a Rust-port bug, never a fixture to loosen. TypeScript is canonical — including where it is arguably wrong, because a port that is "more correct" in isolation is still divergent. Where the shared contract itself needs to change, it changes in all three ports together. This is what lets you gate in Rust and render in TypeScript and trust they decided the same thing. # Server-Side Integration > Use RevTurbine server-side for entitlement checks, placement decisions, and SSR. The headless SDK runs on Node.js and edge runtimes. On a server it evaluates the Playbook snapshot you load, in-process, one user per instance, with no decisioning network call. Your server key authenticates hosted Playbook loading and event ingest. The separate `RevTurbineServer` helper mints [client sessions](/revturbine-sdk/guides/client-sessions/) for the browser; it does not evaluate entitlements. **Note — TypeScript uses RevTurbineUserContext:** The `./headless` entry point accepts the same canonical, flat user shape as the browser and React SDKs. Put the tenant in the top-level `tenantId` option and the identity in `user.id`; do not use the compact Python/Rust `{tenant_id, user_id}` shape here. See the [cross-language context mapping](/revturbine-sdk/user-context/#server-sdk-context-shapes). ## When to Use Server-Side | Use case | Client SDK | Server SDK | |---|---|---| | Render placements in React | ✅ | — | | Gate API endpoints | — | ✅ | | Check a usage allowance before processing | — | ✅ (your application owns atomic consumption) | | SSR placement decisions | ✅ (local mode) | ✅ | | Report events to RevTurbine | ✅ | ✅ TypeScript connected mode; see [Events](#what-a-server-reports) | | Mint client sessions | — | ✅ `RevTurbineServer` | ## Headless Server Re-check Use the `./headless` entry so a server or edge function does not load React. Always repeat money-, data-, or compute-sensitive checks at this boundary. **Load the Playbook once per process, and build one instance per request.** The SDK holds a single user context per instance, so a shared instance would let two concurrent requests overwrite each other's user. A request-scoped instance built from an already-loaded Playbook makes no network call to decide: ```ts title="src/server/revturbine.ts" import { initRevTurbine, RuntimeMode, type RevTurbineUserContext, } from '@revturbine/sdk/headless'; import playbook from './revturbine.playbook.json'; /** One instance per request. The Playbook is loaded once, when this module is. */ export function revturbineFor(user: RevTurbineUserContext) { return initRevTurbine({ runtimeMode: RuntimeMode.LocalOnly, localRuntime: { playbook }, user, }); } ``` ```ts title="src/server/export-route.ts" docs-check=strict import type { RevTurbineUserContext } from '@revturbine/sdk/headless'; import { revturbineFor } from './revturbine'; import { getSignedInUser, runExport } from './app'; // your own code export async function POST(request: Request): Promise { const user = await getSignedInUser(request); if (!user) return new Response('Unauthorized', { status: 401 }); const context: RevTurbineUserContext = { id: user.id, plan_handle: user.planHandle, // from your own billing record: the server decides, not the browser usage: { api_calls: { entitlement_handle: 'api_calls', unit: 'calls', amount: user.apiCallCount, limit: user.apiCallLimit, }, }, }; const rt = await revturbineFor(context); try { const result = await rt.can('data_export'); if (!result.allowed) return new Response('Upgrade required', { status: 403 }); return await runExport(request); } finally { rt.sdk.dispose(); } } ``` The Playbook file comes from your launched Playbook: `revturbine download --live --save ./revturbine.playbook.json`, run in your build or deploy step. A launch reaches a running server when it next loads that file, so restart or redeploy after a launch. Initial `user.usage` entries use the rich `UserUsageEntry` shape shown above. After initialization, call `rt.update({ usage: { api_calls: total } })` with absolute numeric balances, not deltas. ### Hosted delivery on a backend TypeScript's `RuntimeMode.Server` supports a backend `apiKey` for Playbook loading and event ingest. Browser integrations use `publicKey`; browser and worker contexts never send the server secret. A backend public key without an allowed browser origin is not a substitute for a server key. Evaluation still runs locally over `UserContext` and `Playbook`. The application owns requests and any shared snapshot; never mutate a shared user-bound SDK instance to serve different users. The `LocalOnly` pattern above evaluates the snapshot supplied by the application. For an explicitly connected instance, a supplied `localRuntime.playbook` is a startup seed that hosted loading refreshes. A failed load is visible through `getPlaybookLoadFailure()`. Without a readable Playbook, entitlement checks fail closed with `config_unavailable`. ### What a server reports A `LocalOnly` instance makes no hosted decision or event-ingest call. Its `track()` calls do not reach RevTurbine ingest. A TypeScript hosted backend instance authenticates `track()` events with its server key. Call `await rt.flushEvents()` before ending a short-lived operation; backend instances have no browser page-unload lifecycle. `flushEvents()` is best-effort transport work, not a durable delivery receipt; call `rt.sdk.dispose()` in `finally` after using the session. Events belong to that instance's user context. This transport does not turn the SDK into a request handler or share identity between users. Python and Rust evaluate a Playbook supplied by the application and send no events. ## Server-Owned CTA Actions An authored CTA such as `extend_trial` starts in the browser but must mutate state on your server. Register a `serverActions` handler with the client SDK; it calls your route and returns fresh server-authoritative context: ```ts const options = { // ...your normal RevTurbineProvider options serverActions: { extend_trial: async ({ placement, params }) => { const response = await fetch('/api/trial/extend', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ decisionId: placement.decision_id, extensionDays: params.extension_days, }), }); if (!response.ok) return { success: false }; return { success: true, userContext: await response.json() }; }, }, }; ``` The route repeats the entitlement check before it changes anything: ```ts docs-check=strict import { revturbineFor } from './revturbine'; import { extendTrial, requireUser } from './app'; // your own code export async function POST(request: Request): Promise { const user = await requireUser(request); const rt = await revturbineFor({ id: user.id, plan_handle: user.planHandle }); try { const access = await rt.can('trial_extension'); if (!access.allowed) return new Response('Upgrade required', { status: 403 }); const trial = await extendTrial(user.id); return Response.json({ plan_handle: user.planHandle, trial: { in_trial: true, days_remaining: trial.daysRemaining }, }); } finally { rt.sdk.dispose(); } } ``` The SDK contains rejection and `{ success: false }`, leaves held context unchanged, and records the result on the canonical `placement_interaction` event. There is no client-side `extendTrial()` mutation. ## Bundled Local Runtime The local runtime evaluates entitlements and placements entirely from a Playbook JSON snapshot. No API server connection required. ### Installation ```bash pnpm add @revturbine/sdk ``` ### Entitlement Checks ```ts import { initRevTurbine, RuntimeMode } from '@revturbine/sdk/headless'; import playbook from './revturbine.playbook.json'; const sdk = await initRevTurbine({ runtimeMode: RuntimeMode.LocalOnly, localRuntime: { playbook }, user: { id: 'user_123', plan_handle: 'starter', entitlements: { data_export: false }, }, }); // Check an entitlement const result = await sdk.can('data_export'); if (!result.allowed) { // deny access — user needs to upgrade console.log('Upgrade required:', result.status); } ``` ### Placement Decisions (SSR) Resolve placements server-side for SSR or email templates: ```ts import { initRevTurbine, RuntimeMode } from '@revturbine/sdk/headless'; import playbook from './revturbine.playbook.json'; const sdk = await initRevTurbine({ runtimeMode: RuntimeMode.LocalOnly, localRuntime: { playbook }, user: { id: req.user.id, plan_handle: req.user.planHandle, }, }); // Report current usage (balances live under `usage`; a bare handle is not // a usage report) sdk.update({ usage: { api_calls: req.user.apiCallCount } }); // Resolve a specific slot's placement const banner = await sdk.getPlacement({ slotId: 'dashboard_banner' }); if (banner) { // Render banner.content into your template } ``` ## SSR with React (Local Mode) For frameworks like Next.js, resolve placements with the local runtime and hydrate client-side: `sdk.hydrate(payload)` applies user context, entitlements, trial status, and usage. Legacy payload placement decisions are not reused: they lack a binding to the producer's complete evaluation context and Playbook. The browser evaluates placements locally from its current inputs, without a decision HTTP request. ```tsx // components/Dashboard.tsx 'use client'; import { RevTurbineProvider, Slot, RuntimeMode } from '@revturbine/sdk'; import playbook from './revturbine.playbook.json'; import { useMemo } from 'react'; export default function Dashboard({ user }) { const options = useMemo(() => ({ runtimeMode: RuntimeMode.LocalOnly, localRuntime: { playbook }, user: { id: user.id, plan_handle: user.planHandle, entitlements: user.entitlements, }, }), [user]); return ( {/* rest of your dashboard */} ); } ``` ## Client Sessions `RevTurbineServer`, exported from `@revturbine/sdk/headless`, holds your server key and mints short-lived, browser-safe client sessions for one user at a time. This is the hosted-mode job for your server: it is how plan, trial and payment state from Stripe reaches the browser SDK. It is not a decision evaluator; use the headless session above for checks. ```ts title="app/api/revturbine-session/route.ts" import { RevTurbineServer } from '@revturbine/sdk/headless'; import { getSignedInUser } from '@/server/auth'; // your own auth: the signed-in user, or null const revturbine = new RevTurbineServer({ tenantId: process.env.REVTURBINE_TENANT_ID!, apiKey: process.env.REVTURBINE_API_KEY!, // the server key, never NEXT_PUBLIC_ endpoint: process.env.REVTURBINE_ENDPOINT ?? 'https://revturbine.com/app', }); export async function POST(): Promise { const user = await getSignedInUser(); if (!user) return new Response('Unauthorized', { status: 401 }); const { client_token } = await revturbine.clientSessions.create({ subject: user.id }); return Response.json({ client_token }); } ``` The browser half, an Express version, the failure behavior, and how the SDK refreshes the token are on [Client sessions](/revturbine-sdk/guides/client-sessions/). ## Server SDK languages TypeScript has both browser/React and headless entry points. Python and Rust are headless libraries for your backend; all three evaluate the same Playbook with parity-checked decisions. | Language | Package | Guide | |---|---|---| | Node.js / edge | `@revturbine/sdk/headless` | This page; [Headless API](/revturbine-sdk/guides/headless-api/) | | Python | `revturbine` (PyPI) | [Python SDK](/revturbine-sdk/getting-started/python/) | | Rust | `revturbine` (crates.io) | [Rust SDK](/revturbine-sdk/getting-started/rust/) | TypeScript supports local mode everywhere and hosted Playbook delivery in both browsers and trusted backends. A backend uses its server `apiKey` for delivery and evaluates locally in a request-scoped session; the separate `RevTurbineServer` helper mints browser client sessions. Python and Rust evaluate a supplied Playbook locally; the application owns refreshing that snapshot. Their guides describe installation, input shapes and the supported surfaces. Python exposes the same re-check question as `can()`: ```python result = sdk.can("data_export") if not result["allowed"]: raise PermissionError(result["reason"]) ``` See the [Python SDK guide](/revturbine-sdk/getting-started/python/) and [Rust SDK guide](/revturbine-sdk/getting-started/rust/) for setup and usage. ## Next Steps - [User Context](/revturbine-sdk/user-context/) — canonical fields and cross-language shapes - [Entitlements Guide](/revturbine-sdk/guides/entitlements/) — entitlement types and checking patterns - [Runtime Modes](/revturbine-sdk/guides/runtime-modes/) — choosing the right runtime mode - [Headless API](/revturbine-sdk/guides/headless-api/) — imperative SDK usage without React ## Runnable cloud examples Follow [Cloud and edge entitlement checks](/revturbine-sdk/tutorials/vercel-edge-entitlements/) for Vercel, Firebase, AWS, Google Cloud and Azure. It keeps authentication and request handling in your application, caches only immutable Playbooks, and constructs a fresh SDK session for each operation. The [Vercel walkthrough](/revturbine-sdk/tutorials/vercel-edge-entitlements/) also exercises the browser-compatible bundle in an Edge VM. # Beyond Gating > Gating one feature is the on-ramp. The control plane also does targeting, priority across competing placements, lifecycle triggers, and explainability — all editable without a deploy. Gating one feature is the first thing you'll do. It's also the smallest thing RevTurbine does. One layer in, the control plane is deciding *what to show, to whom, and when* — and all of it moves in config, not code. ## Targeting Every entitlement rule and placement can be scoped to a **segment** — a rule over user-context traits. That's how one placement shows different copy to different users, and how you [run experiments](/revturbine-sdk/guides/experiments/) by matching a variant assignment. Your app sets the traits; RevTurbine does the matching. ## Priority across competing placements Multiple placements can be eligible for the same moment. `getPlacementDecision()` picks **one winner** using eligibility, presentation caps, cooldowns, and priority — so a user never gets three upsells stacked on top of each other. You render the winner with a ``; the arbitration happens in the engine. ## Lifecycle triggers Beyond "is this allowed?", you can tell the engine *what just happened* with `emitTrigger()` — canonical lifecycle events it recognizes and can respond to with the right placement: ```ts import { useRevTurbine } from '@revturbine/sdk'; const { sdk: rt } = useRevTurbine(); // with a headless session: `session.sdk` await rt.emitTrigger('usage_limit_approaching', { usage_percent: 85, threshold: 80 }); await rt.emitTrigger('trial_expiring', { days_remaining: 2 }); await rt.emitTrigger('feature_gated', { feature: 'advanced_automation' }); ``` The engine attaches standard context (user, plan, timestamp) and decides whether — and which — placement to surface, so a "trial ending" nudge or a "running low" banner appears at the right moment without you hard-coding the timing. ## Explainability When you need to know *why* a decision resolved the way it did, [`explainPlacementDecision()`](/revturbine-sdk/operate/debugging/) returns the full reasoning — segment matches, entitlement outcomes, and suppression. It's the same payload the engine used, so you can build QA tooling or a "why am I seeing this?" panel on it. ## Experimentation Segment-driven experimentation is shipped today: bring a variant assignment from your experimentation platform and RevTurbine branches the monetization response on it — see [Run an Experiment with Segments](/revturbine-sdk/guides/experiments/). **Note — Roadmap: ML-scored optimization:** Today you decide the strategy and RevTurbine executes it deterministically. **ML-scored propensity and auto-optimization** — RevTurbine choosing the winning variant or the best next offer for you — are on the roadmap, not shipped. ## Automate it Because everything above is config, a coding agent can drive it on a cadence with deterministic guardrails — see [Add RevTurbine with a Coding Agent](/revturbine-sdk/guides/coding-agent/). ## Related - [Placements](/revturbine-sdk/guides/placements/) — decision inputs, caps, and priority in depth - [Run an Experiment with Segments](/revturbine-sdk/guides/experiments/) - [Debugging Decisions](/revturbine-sdk/operate/debugging/) # Billing & Stripe > Connect your Stripe account, set up webhook destinations, and verify the pipeline in test mode. RevTurbine reads billing truth from Stripe — the customer's current plan, payment health, and subscription lifecycle all flow in through Stripe webhooks and land on the customer record, where decisions read them. Stripe is the supported billing provider today; others are planned. This page covers the three operational pieces: connecting the account, wiring the webhook destinations, and proving the pipeline works before real money moves. ## Connect your Stripe account Connection happens in the dashboard — **Settings → Stripe**. There are two paths, and neither involves pasting API keys: - **Existing Stripe account** — use **Connect with Stripe** (OAuth). You're sent to Stripe, approve the connection, and land back in Settings with the account linked. RevTurbine receives scoped access through Stripe Connect; you can revoke it from either side at any time. - **New Stripe account** — use **Set up payments** (embedded onboarding). Stripe's onboarding flow runs inside the dashboard via an account session; when Stripe finishes provisioning, the account is linked automatically. **Note:** RevTurbine never asks for a Stripe secret key. If an integration guide tells you to paste one, it isn't describing RevTurbine. Once connected, plan variations link to Stripe prices (`stripe_price_id`), the billing page gains a **Sync** action that compares RevTurbine's plans against your Stripe catalog, and webhook events begin updating customer records. ## Webhooks: nothing to register RevTurbine is a Stripe Connect platform. When you connect, Stripe starts delivering your account's events to RevTurbine's platform-level webhook endpoints — subscription and invoice events, payment-method and person/account events — and RevTurbine verifies each one and resolves it to your tenant from the connected account it came from. There are no destinations to create in your Stripe Dashboard, no signing secrets to copy, and no URL to configure. If billing state stops updating, the first place to look is **Settings → Stripe** in the RevTurbine dashboard: the connection status and the events-received summary show whether events are arriving. ### Link Stripe customers to accounts Stripe knows who paid. RevTurbine knows which account and user used the product. Until each paying Stripe customer is linked to one of your accounts, that revenue is a payer nobody can attribute: it is missing from cohort and retention figures, and no conversion can be credited to the prompt that drove it. Link them from day one, in your own Stripe calls. **RevTurbine never writes to your Stripe account**; the metadata below is yours to set. RevTurbine looks for these three pieces of evidence for each payer: 1. **Customer metadata.** `revturbine_account_id` (preferred), or `revturbine_user_id`, which resolves through your users to their account. 2. **An existing link** between the Stripe customer and an account. 3. **A customer your Checkout created**, resolved through the session's metadata or `client_reference_id`. They are not a priority list where the first hit silently wins. RevTurbine collects all that exist, and when they agree on one account the payer is linked. **When they point to two different accounts, the payer is left unlinked** until you correct the data. Keep the keys, the Checkout metadata and your customer records consistent. Matching a payer by email is not available: no SDK call writes a user's email to RevTurbine from your server yet, so there is nothing to match against. Do not rely on `identify()` with an `email` to link a payer. The first is explicit and the only one under your control from the start, so **use the `revturbine_*` keys as the primary path** and set them everywhere you create a customer. Use the same values you give the SDK: `revturbine_user_id` is the `user.id`, and `revturbine_account_id` is the `account_id`. **Fallback for billing layers such as Clerk.** When none of the three apply, a plain `user_id` in the Customer metadata is used as a stand-in account identity. It is a reasonable proxy for `account_id`, but it is **not a RevTurbine user id**: RevTurbine user ids come from the user context you pass to the SDK, never from this key. Do not rely on it when you can set `revturbine_account_id`. ```ts docs-check=strict import Stripe from 'stripe'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); /** Your signup or first-checkout code: create the customer already linked. */ export async function createStripeCustomer( user: { id: string; email: string }, accountId: string, ) { return stripe.customers.create({ email: user.email, metadata: { revturbine_account_id: accountId, // the account_id you pass to the SDK revturbine_user_id: user.id, // the user.id you pass to the SDK }, }); } /** Customers that already exist: a one-off backfill, then keep it on every update. */ export async function linkExistingCustomer( stripeCustomerId: string, user: { id: string }, accountId: string, ) { return stripe.customers.update(stripeCustomerId, { metadata: { revturbine_account_id: accountId, revturbine_user_id: user.id }, }); } ``` When Checkout is what creates the customer, Stripe cannot set Customer metadata inline. Create the customer first and pass `customer`, so the link exists before the purchase: ```ts docs-check=strict import Stripe from 'stripe'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); export async function startCheckout( user: { id: string }, accountId: string, stripeCustomerId: string, stripePriceId: string, ) { const ids = { revturbine_account_id: accountId, revturbine_user_id: user.id }; return stripe.checkout.sessions.create({ mode: 'subscription', customer: stripeCustomerId, // created above, with the metadata line_items: [{ price: stripePriceId, quantity: 1 }], client_reference_id: user.id, metadata: ids, // on the session subscription_data: { metadata: ids }, // and on the subscription success_url: 'https://app.example.com/billing/done', }); } ``` If you cannot pre-create the customer (a Checkout in `payment` mode, say, with `customer_creation: 'always'`), copy the same two keys onto the Customer from your `checkout.session.completed` handler with `stripe.customers.update`. The placement attribution bag from `checkoutMetadata()` below goes on the same session and subscription metadata. #### Use the same account id in the SDK Pass the same `account_id` to the SDK that you put in `revturbine_account_id`, so what the SDK reports and what Stripe bills describe the same account. Never use an email as the id. ```tsx docs-check=strict import { useRevTurbine } from '@revturbine/sdk'; import { useEffect } from 'react'; /** Mount once, inside the provider, once the signed-in user is known. */ export function IdentifyUser({ user }: { user: { id: string; accountId: string } }) { const { sdk } = useRevTurbine(); useEffect(() => { if (!sdk) return; // the SDK is null until the provider has initialized sdk.identify(user.id, { account_id: user.accountId }); }, [sdk, user.id, user.accountId]); return null; } ``` The same field works at init (`user: { id, account_id }` on `RevTurbineProvider`'s `options`) and through `setUserContext()` / `update()`. #### What linking unlocks - **Cohort and retention figures** for paying accounts, because revenue now belongs to an account with a signup date and usage. - **Attribution**: a purchase is credited to the placement the user saw, even for renewals and checkout-less billing events, with no time-window guess. - **Server-derived plan state** in the browser, through the [client session](/revturbine-sdk/guides/client-sessions/). A payer with none of these links cannot be attributed to an account until one exists, so set the metadata before the first purchase rather than after. ### What the events drive - `customer.subscription.created/updated/deleted` → the customer's current plan — which flows into SDK decisions automatically: the client context carries the server-derived plan handle, and hosted evaluation defaults to it when the caller doesn't supply one. - `invoice.payment_failed` / `payment_intent.payment_failed` → the customer's billing-health issues, which drive the `payment_failed` retention qualifier server-side (and clear again on `invoice.payment_succeeded`). - Account/person (v2) events → connected-account status shown in Settings. ## Verify in test mode Walk this once after connecting, before anything real depends on the pipeline: 1. **Fire synthetic events on your connected account.** With the Stripe CLI, in test mode: ```bash stripe trigger customer.subscription.created --stripe-account acct_… stripe trigger invoice.payment_failed --stripe-account acct_… ``` 2. **Watch them land.** Settings → Stripe shows an events-received summary; a triggered subscription event should move a test customer's plan, and the payment-failure event should flip that customer's billing health to `attention_required` (visible on the customer record). 3. **Run the catalog comparison.** The billing page's **Sync** action compares RevTurbine plans against the Stripe catalog and reports mismatched, missing, and orphaned prices — a clean report means plan↔price linkage is coherent. 4. **Confirm recovery.** `stripe trigger invoice.payment_succeeded` should clear the billing-health issue again. ## Integration settings **Settings → Stripe** is the home for the connection itself: connected-account status, the events-received summary, and the Sync comparison. Stripe is the first integration with a settings surface; outbound analytics forwarding (PostHog — `createPostHogIntegration` in the SDK) is configured in code today, with a settings surface planned. ## Where checkout fits RevTurbine decides *what* to offer; your app owns the checkout itself. A CTA with the `open_checkout` action resolves to `open_checkout_modal` carrying the plan handle — your resolver opens Stripe Checkout (or your own flow) for the matching price. After payment, the subscription webhook updates the customer's plan, and access follows without further app code. ### Tell Stripe which placement led here Your server creates the Checkout Session, and Stripe only knows what you put on it. `checkoutMetadata(outputId)` returns the `revturbine_*` bag for the output the user clicked — the user, account, placement, decision, output, rule and target plan the SDK holds for it — so the purchase joins the presentation and click it came from with **confirmed** credit instead of a time-window inference. Send it to your server with the price the user chose: ```ts import { useRevTurbine } from '@revturbine/sdk'; // Browser: `output` is the decision the slot rendered (`decision.output`). const { sdk: rt } = useRevTurbine(); // with a headless session: `session.sdk` const checkout = rt.checkoutMetadata(output.output_id); // `checkout` is null for an output this SDK did not decide. ``` Then put the bag on the session **and** on the subscription, and the user id on `client_reference_id`: ```ts docs-check=false reason="server-side Stripe SDK call; the stripe package is not part of the SDK toolchain" // Server: `checkout` is what the browser sent. const session = await stripe.checkout.sessions.create({ mode: 'subscription', line_items: [{ price: stripePriceId, quantity: 1 }], customer: stripeCustomerId, client_reference_id: checkout.client_reference_id, metadata: checkout.metadata, subscription_data: { metadata: checkout.metadata }, }); ``` - `metadata` on the session is what `checkout.session.completed` carries. - `subscription_data.metadata` mirrors the bag onto the Subscription, so every `customer.subscription.*` event carries it and every invoice snapshots it under `parent.subscription_details.metadata` — renewals, failed payments and recoveries stay attributed to the placement without a customer mapping. - `client_reference_id` is Stripe's own caller-side key for the session; RevTurbine reads it as the user id when the metadata is absent. The bag's keys are `revturbine_user_id`, `revturbine_account_id`, `revturbine_placement_id`, `revturbine_decision_id`, `revturbine_output_id`, `revturbine_rule_id` and `revturbine_plan_handle`. A key the SDK does not hold is absent rather than empty — no id is ever invented — so an unidentified user yields no `revturbine_user_id` and no `client_reference_id`, and a CTA that names no plan yields no `revturbine_plan_handle`. **Caution — Customer metadata cannot be set from Checkout:** Checkout cannot set metadata on the Stripe Customer inline. Create the Customer yourself with `metadata.revturbine_user_id` before opening Checkout (see above), or, if you let Checkout create it, follow up with `customers.update({ metadata })` from your `checkout.session.completed` handler. The session and subscription copies of the bag identify the purchase either way; the Customer copy is what makes later, checkout-less billing events resolvable without a customer mapping. # Client sessions > The full round trip for hosted mode — your server route that mints a session token with the server key, the browser callback that fetches it, and what the SDK does with it. In hosted mode RevTurbine knows things your browser does not: the plan Stripe says this user is on, whether a trial is running, whether a payment failed. A **client session** is how that state reaches the SDK without you wiring a refresh loop. It has two halves, and you write both: - a **route on your server** that checks who is signed in, mints a short-lived token with your server key, and returns it; - a **callback in the browser** that calls that route and hands the token to the SDK. ```text browser your server RevTurbine | POST /api/revturbine-session | | |------------------------------->| who is signed in? (your auth) | | | POST /api/sdk/client-sessions | | | (server key, subject = user) | | |------------------------------->| | |<-- { client_token, expires_at }| |<-- { client_token, expires_at }| | | GET /api/sdk/client-context (Bearer client_token) | |---------------------------------------------------------------->| |<-- plan, trial, payment health (client-safe fields only) | ``` The server key never leaves your server. The browser holds only the token, in memory, for about ten minutes. ## The server route The route does three things in order: authenticate **your** user, mint with **your** server key, return **only** the token. 1. **Authenticate with your own auth.** RevTurbine does not know who is signed in to your app. Mint for the user your session says is signed in, and take the id from your session, never from the request body: the token carries that subject, and the SDK will apply whatever plan it resolves to the user it is initialized for. 2. **Mint with the server key.** `RevTurbineServer` holds the key and calls `POST /api/sdk/client-sessions` for you. Create it once per process. 3. **Return only `client_token`** (and `expires_at` if you want it). Do not return the server key, the tenant id, or any other secret. Create the server object once, in a module of its own: ```ts title="src/server/revturbine-server.ts" import { RevTurbineServer } from '@revturbine/sdk/headless'; export const revturbineServer = new RevTurbineServer({ tenantId: process.env.REVTURBINE_TENANT_ID!, apiKey: process.env.REVTURBINE_API_KEY!, // the server key. Never NEXT_PUBLIC_. endpoint: process.env.REVTURBINE_ENDPOINT ?? 'https://revturbine.com/app', }); ``` ### Next.js route handler ```ts title="app/api/revturbine-session/route.ts" import { revturbineServer } from '@/server/revturbine-server'; import { getSignedInUser } from '@/server/auth'; // your own auth: the signed-in user, or null export async function POST(): Promise { const user = await getSignedInUser(); if (!user) return new Response('Unauthorized', { status: 401 }); try { const { client_token, expires_at } = await revturbineServer.clientSessions.create({ subject: user.id, }); return Response.json({ client_token, expires_at }, { headers: { 'cache-control': 'no-store' } }); } catch { // The error carries only a status and a request id, never the key. return new Response('Could not create a RevTurbine session', { status: 502 }); } } ``` ### Express ```ts title="src/server/routes/revturbine-session.ts" import express from 'express'; import type { Request, Response } from 'express'; import { revturbineServer } from '../revturbine-server'; import { requireSignedIn } from '../auth'; // your own auth middleware: sets req.user or answers 401 export const revturbineSession = express.Router(); revturbineSession.post('/api/revturbine-session', requireSignedIn, async (req: Request, res: Response) => { try { const { client_token, expires_at } = await revturbineServer.clientSessions.create({ subject: req.user.id, }); res.set('cache-control', 'no-store').json({ client_token, expires_at }); } catch { res.status(502).send('Could not create a RevTurbine session'); } }); ``` Use this one route name everywhere: `/api/revturbine-session`. The SDK does not care what it is called, but your docs, your CSRF rules and your team will. **Note — What the token can do:** A client session carries one capability, `context:read`: it reads the client-safe context for the user it was minted for, and nothing else. It cannot write, cannot read another user, and cannot call a management API. It expires after about ten minutes. Server-only detail, such as raw Stripe ids and failed payment records, is never in the response. ## The browser callback `clientSession` is a **function**, not a token. The tokens are short-lived, so a value captured at init would go stale mid-session. ```ts title="src/revturbine-client-session.ts" export async function fetchClientSession(): Promise { const response = await fetch('/api/revturbine-session', { method: 'POST', credentials: 'same-origin', }); if (!response.ok) { throw new Error(`RevTurbine client session failed (${response.status})`); } const body: { client_token: string } = await response.json(); return body.client_token; } ``` Then pass it at init, as on [Initialize the SDK](/revturbine-sdk/getting-started/initialization/): ```tsx title="src/revturbine-provider.tsx" import { RevTurbineProvider } from '@revturbine/sdk'; import { useMemo, type ReactNode } from 'react'; import { fetchClientSession } from './revturbine-client-session'; export function RevTurbine({ userId, children }: { userId: string; children: ReactNode }) { const options = useMemo( () => ({ publicKey: process.env.NEXT_PUBLIC_REVTURBINE_PUBLIC_KEY!, user: { id: userId }, clientSession: fetchClientSession, }), [userId], ); return {children}; } ``` ## What the SDK does with it 1. It calls your callback **the first time it needs a token**, which is at init. 2. It sends the token as a Bearer credential to `GET /api/sdk/client-context`, and folds the plan, trial and payment-health fields it gets back into the user's context. Decisions then re-evaluate, locally. 3. When you call `identify()` with a new user, it drops the held token and calls your callback again, because a token is minted for one user. 4. When RevTurbine answers `401` (the token expired or was revoked), it calls your callback once for a fresh token and retries once. It does not loop. The token is held in memory only. It is never persisted, never logged, never put in a URL, and never merged into the user context you can read. ### When something goes wrong Enrichment is best-effort, and it never breaks your app. | What fails | What happens | |---|---| | Your route returns an error or your callback throws | The SDK keeps the context you passed at init. No plan arrives from Stripe, so pass `plan_handle` yourself if your signed-out or error state needs one. | | Your callback returns an empty string | Treated as no token: same as above. | | The token expires mid-session | One automatic re-mint and retry. | | A different user signs in | Call `identify(newId)` (or re-create the provider, as the example above does when `userId` changes): the held token is dropped and your callback mints one for the new user. | | Nobody is signed in | Your route answers `401`, so the callback throws and the SDK keeps the context you passed. | | You omit `clientSession` | Nothing is requested. Server-derived plan and trial state never reach the SDK. | Because failures are silent by design, check the route yourself: sign in, open the network tab, and confirm `POST /api/revturbine-session` answers `200` with a `client_token`, followed by `GET …/api/sdk/client-context` answering `200`. ## Which language mints TypeScript on Node mints today, with `RevTurbineServer` as above. The Python and Rust packages evaluate a Playbook you supply; they do not mint client sessions. If your backend is Python or Rust, call `POST /api/sdk/client-sessions` yourself with the server key as a Bearer token and `{ "subject": "" }` as the JSON body. It answers `201` with `{ client_token, expires_at }`; return the `client_token` to the browser. ## Related - [Initialize the SDK](/revturbine-sdk/getting-started/initialization/) - [Configuration reference → `clientSession`](/revturbine-sdk/reference/configuration/#server-derived-context-clientsession) - [Production Readiness → Keys](/revturbine-sdk/operate/production-readiness/#keys) - [Billing & Stripe](/revturbine-sdk/guides/billing/) # Cloud and Edge Entitlements > Run the same trusted UserContext and Playbook pattern across five cloud providers. The SDK evaluates a **UserContext and Playbook in your application**. It can run in a browser, a regional server function, or a compatible edge runtime. RevTurbine delivers configuration; your application's protected operation enforces the result. The [complete example](https://github.com/revt-eng/revturbine-sdk/tree/main/examples/cloud-entitlements) uses the published SDK 0.12.4 and includes local tests. Start with the [cloud export tutorial](/revturbine-sdk/tutorials/vercel-edge-entitlements/), then choose an adapter. | Provider | Server implementation | Edge implementation | | --- | --- | --- | | Vercel | Node.js Function | Standalone Edge Function | | Firebase | Functions v2 | Hosting CDN rewrites to the regional function | | AWS | Lambda with HTTP API v2 | Lambda@Edge origin-request; not CloudFront Functions | | Google Cloud | Cloud Run HTTP container | Global load balancer routes to regional Cloud Run | | Azure | Functions v4 HTTP | Front Door routes to regional Functions | The CDN/front-door variants for Firebase, Google Cloud, and Azure do not execute the SDK at the edge. They preserve the same authenticated regional enforcement. ## Shared request lifecycle 1. Authenticate the application-issued context token using a trusted JWKS endpoint. 2. Validate its issuer, audience, tenant, subject, type, and short expiry. 3. Load a verified, bounded Playbook snapshot for that deployment's tenant. 4. Initialize a fresh SDK session with that user's authoritative context. 5. Check the entitlement, enforce resource ownership, and perform the operation. 6. Dispose the session in `finally`, including denied and failed operations. Only immutable Playbook snapshots and public keys are shared across requests. Never keep a mutable SDK session in a global variable and switch users between requests. Ignore client-supplied plan or account claims unless authenticated by your trusted issuer; a signed token must originate from authoritative records. ## Cache and security contract The example cache uses authenticated HTTPS bootstrap, approved HTTPS origins, tenant/environment checks, manifest validity, schema compatibility, byte length, and SHA-256 integrity. It refreshes within 60 seconds and never beyond manifest expiry. A failed refresh returns 503; it does not grant from stale policy. It does not verify the optional Ed25519 manifest signature. This is an online, authenticated-bootstrap trust model, not offline signature verification. Context tokens expire within five minutes. This bounds, but does not eliminate, the delay before a changed subscription or revoked membership becomes visible. Choose shorter tokens or an application revocation check when needed. These are application-issued JWTs, not `rt_client_` RevTurbine client-session tokens. They have separate issuers, audiences, and verification paths. Delivery uses a four-second timeout **per fetch**, while JWKS resolution has its own three-second timeout. These are not a total-operation deadline; budget for sequential requests and enforce your application's overall timeout separately. Do not cache personalized responses at a CDN. The adapters emit `private, no-store`; also disable the provider's route caching and forward Authorization. Keep server tokens out of public bundles, logs, URLs, and downstream CDN requests. The sample `data_export` entitlement is boolean. Metered operations additionally need atomic application-side quota reservation/consumption. An SDK check followed by a separate write is not an atomic quota transaction. ## Local validation From `examples/cloud-entitlements`, run `npm ci`, `npm run typecheck`, and `npm test`. Tests build adapters, exercise realistic HTTP/event boundaries, verify JWTs and real SDK decisions, and execute the browser bundle in Edge Runtime. No cloud accounts, deployments, or live credentials are required for acceptance. See the example README for configuration, packaging, and provider limitations. Provider deployment remains an optional adoption step, not a claim made by the local tests. Compare this cache with the simpler [native server initialization](/revturbine-sdk/getting-started/initialization/). ## Provider references - [Vercel Edge runtime](https://vercel.com/docs/functions/runtimes/edge) - [Firebase Hosting function rewrites](https://firebase.google.com/docs/hosting/functions) - [Lambda@Edge restrictions](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-at-edge-function-restrictions.html) - [HTTP API payload formats](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html) - [Cloud Run container contract](https://cloud.google.com/run/docs/container-contract) - [Azure Functions Node programming model](https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-node) - [Front Door caching](https://learn.microsoft.com/en-us/azure/frontdoor/front-door-caching) # Add RevTurbine with a Coding Agent > Paste-in prompts to add RevTurbine to an existing app or build it into a new one with Claude Code, Cursor, or your agent of choice — plus how to automate the recurring config loop through the CLI with deterministic guardrails. If you integrate through a coding agent, you don't have to hand-wire anything. Pick the prompt that matches your situation — adding RevTurbine to an app you already have, or building a new app with RevTurbine from the start. The agent does the inspection and the edits, and you review the diff. ## Existing app: replace one plan check ```text You're adding RevTurbine (@revturbine/sdk) to this app to gate one paid feature. 1. Inspect the app and find ONE non-critical feature currently gated by a hard-coded plan check (e.g. `user.plan === 'pro'`). Pick the safest one. 2. Run `npm create revturbine@latest`. It installs @revturbine/sdk, pins the CLI and writes a starter revturbine.playbook.json. Wrap the app in in local_only mode with that Playbook, so it runs with no account. 3. Edit the Playbook so it has a free and a pro plan, one entitlement for the feature, and a gate placement (upgrade modal) for free users. Refer to plans and entitlements by their unique_handle. Run `npx revturbine validate ./revturbine.playbook.json` until it reports no errors or warnings. 4. Replace the hard-coded check with , keeping the existing UI as the gated children and letting the placement render the upsell. 5. Do NOT touch any billing-critical or server-side enforcement path. Leave those checks in place — the client Gate is UI only. 6. Run the type checker, then explain the diff: what moved into config, what stayed in code, and why the server-side check (if any) was left alone. ``` **Tip — Why 'one non-critical feature':** The prompt deliberately scopes the agent to a single, safe feature and forbids touching enforcement paths. Monetization changes ship as config, so the first change should be reversible and low-stakes. See [Client vs Server Enforcement](/revturbine-sdk/concepts/enforcement/). ## New app: build RevTurbine in from the start ```text You're building a new app with RevTurbine (@revturbine/sdk) handling its plans, entitlements, usage limits and upgrade prompts. 1. Scaffold the app with its framework first (e.g. create-next-app), then run `npm create revturbine@latest` inside it. It installs the SDK, pins the CLI, writes a starter revturbine.playbook.json and installs the RevTurbine agent skills. Follow the revturbine-start-here skill. 2. Put monetization in the Playbook, not in code: every plan, entitlement, usage limit and upgrade or sign-in prompt. No plan names, prices or limit numbers in app code. Build on the starter Playbook and refer to plans and entitlements by their unique_handle. 3. Run `npx revturbine validate ./revturbine.playbook.json` after every Playbook change and fix every error and warning. 4. Start in local_only mode with the bundled Playbook. Use or can() for access, for prompts, and update() to report usage as the running total for the current period (your app keeps that count). 5. Re-check every paid or billable action on the server with @revturbine/sdk/headless before it runs, and answer a denial with 402. Client checks only drive the UI. 6. If people can use the app before signing in, give them a stable id your server can see (for example a cookie) and pass it as user.id. 7. When the docs don't cover something you need, say so and ask. Don't guess. 8. Run `npx revturbine generate types ./revturbine.playbook.json --out src/revturbine-handles.ts` and the type checker, then explain what lives in the Playbook, what lives in code, and what's left for hosted mode. ``` **Tip — Why the Playbook comes first:** Everything in the Playbook can change without a deploy once you move to hosted mode, so a new app should never hard-code what RevTurbine decides. The validator catches a reference that would silently grant nothing. See [Runtime Modes](/revturbine-sdk/guides/runtime-modes/) and [Client vs Server Enforcement](/revturbine-sdk/concepts/enforcement/). ## Automate the config loop with the CLI Everything RevTurbine decides lives in the Playbook, and the CLI (`@revturbine/cli`, command `revturbine`) manages the Playbook lifecycle from the terminal — so a customer's own agent can enhance monetization on a cadence with deterministic guardrails. Anything you can do in the dashboard, you can do through the CLI. A recurring loop looks like: ```bash # 1. Your agent proposes a config change (new segment, adjusted limit, new copy). # 2. Validate it against the real schema before anything ships. revturbine validate ./revturbine.playbook.json # 3. Upload it as the open draft, then preview and evaluate that draft. revturbine upload ./revturbine.playbook.json revturbine preview revturbine evaluate --draft --user ./user-context.json --entitlement data_export # 4. Launch it through the review lifecycle. revturbine launch --draft ``` The guardrails are the point: the agent proposes, the CLI **validates against its pinned schema** and lets you **preview the impact** before launch, and the draft becomes a Release through the same guarded lifecycle a human uses. Your app code never changes — the agent only ever edits the Playbook. **Note — Command surface:** The exact command set, selectors, and flags live in the [CLI reference](/revturbine-sdk/reference/cli/). Treat `revturbine --help` from the project-pinned CLI as authoritative. ## Related - [Replace Hard-Coded Plan Checks](/revturbine-sdk/guides/migrate-plan-checks/) — the before/after the agent produces - [Beyond Gating](/revturbine-sdk/guides/beyond-gating/) — what the config can express - [Client vs Server Enforcement](/revturbine-sdk/concepts/enforcement/) — the paths the agent must not touch # Custom CTA Actions and Prices > Keep authored CTA configuration, resolved actions, and display price units distinct. The authored Studio CTA shape uses `label`, `path`, and optional string-valued `config`. The selected action resolves into a placement's `cta_path`. Do not invent a nested `config.ui_path` object to select a custom action. ```json title="A Studio CTA fragment, not a complete Playbook" { "label": "Open account settings", "path": "open_account_settings", "config": { "tab": "billing" } } ``` For a custom/unmapped path, the resolver carries the action name into the resolved path's `type` and preserves the configured parameters. The SDK's CTA registry normalizes those parameters for the registered action. Register your resolver under the same action name. A label is presentation text, not a dispatch key. The renderer reads normalized `cta_path` and accepts `ui_path` as a compatibility alias. Those output fields are distinct from an authored Studio CTA's `path`/`config` and from a payload's reference to `content_ui_paths`. Follow the [CTA action guide](/revturbine-sdk/guides/placements/#cta-actions) for resolver registration. The resolver performs your application's navigation or backend call; authoring a CTA does not implement that action automatically. ## Display prices use currency minor units Supply `price_amount` using the currency's minor-unit convention. For USD, `4900` displays as $49.00; it is not $4,900. Other currencies can have different fraction digits, so do not unconditionally divide every currency by 100. The schema accepts a nonnegative number; that structural validation alone does not prove the amount uses the right currency units. Playbook prices are display metadata. Stripe or your billing backend determines what is actually charged. Pass a trusted price identifier into checkout and verify it server-side rather than charging an amount from a browser-rendered card. See [source of truth](/revturbine-sdk/concepts/source-of-truth/). Test the resolved action type and parameters, missing-resolver behavior, a successful action, a rejected backend action, and price formatting for each currency your application supports. # Custom Slot Types > Register custom placement components beyond the built-in slots. The SDK ships with 11 built-in slot components (banner, modal, toast, etc.). When those don't fit your use case, you can register custom slot types. ## Built-In Slot Types | ID | Component | Component Type | |---|---|---| | `banner` | `BannerSlot` | `banner` | | `modal` | `ModalSlot` | `modal` | | `toast` | `ToastSlot` | `toast` | | `inline_embed` | `InlineEmbedSlot` | `inline_embed` | | `button` | `ButtonSlot` | `button` | | `quota_meter` | `QuotaMeterSlot` | `quota_meter` | | `full_page` | `FullPageSlot` | `full_page` | | `cli` | `CliSlot` | `cli` | | `credit_balance` | `CreditBalanceSlot` | `credit_balance` | | `tooltip` | `TooltipSlot` | `tooltip` | | `agent_connector` | `AgentConnectorSlot` | `agent_connector` | :::tip[Visual gallery] Every built-in slot has a co-located `.stories.tsx` file in `web-sdk/placements/slots/`. Run `pnpm storybook` in the SDK repo to preview all variants, or try the [Playground](/revturbine-sdk/playground/) for live Sandpack demos. ::: ## Registering a Custom Slot Create a `PlacementSlotType` definition and register it: ```tsx import { PlacementTypeRegistry, useRevTurbineTheme } from '@revturbine/sdk'; import type { PlacementSlotProps } from '@revturbine/sdk'; // 1. Define the component function FeedbackWidget({ content, onDismiss, onCtaClick, exposureRef }: PlacementSlotProps) { const theme = useRevTurbineTheme(); return (

{content?.body}

); } // 2. Register it const registry = new PlacementTypeRegistry(); registry.register({ id: 'custom:feedback-widget', label: 'Feedback Widget', description: 'In-app feedback collection prompt', // One of the SDK's component types — see the table below. componentType: 'in_page', component: FeedbackWidget, priority: 10, // The template id lives on the decision's surface, not at the top level. accepts: (output) => output.surface.template === 'feedback_v1', }); ``` ### Registration Fields | Field | Type | Required | Description | |---|---|---|---| | `id` | `string` | ✅ | Unique identifier (prefix with `custom:`) | | `label` | `string` | ✅ | Human-readable label (shown in the dashboard) | | `description` | `string` | ✅ | What this slot type does | | `componentType` | `RevTurbineComponentType` | ✅ | Canonical component type | | `component` | `ComponentType` | ✅ | React component to render | | `accepts` | `(output) => boolean` | — | Predicate to match specific placements | | `priority` | `number` | — | Higher = evaluated first (default: 0) | | `defaultProps` | `Partial` | — | Default props merged into component | ## PlacementSlotProps Contract All slot components (built-in and custom) receive the same props: ```ts interface PlacementSlotProps { placement: PlacementOutput; content: ResolvedContent; uiPath: PlacementUiPath; promotion?: PlacementPromotion; // Renderer callbacks onCtaClick: () => void; onSecondaryCtaClick?: () => void; onDismiss: () => void; onRemindLater?: () => void; visible: boolean; className?: string; style?: React.CSSProperties; exposureRef?: (element: Element | null) => void; } ``` :::caution Never narrow the `PlacementSlotProps` interface — only extend it. Custom slots should accept all props even if they don't use them. ::: ## Using the Registry Pass your registry to ``: ```tsx ``` The SDK evaluates registered types by priority, calling `accepts()` on each until one matches. ## Theme Integration Custom slots obtain the active theme with `useRevTurbineTheme()`: ```tsx function CustomCard({ content, onCtaClick }: PlacementSlotProps) { const theme = useRevTurbineTheme(); return (

{content?.header}

{content?.body}

); } ``` ## Next Steps - [Theming Guide](/revturbine-sdk/guides/theming/) — customize colors, typography, and shapes - [Component Gallery](/revturbine-sdk/components/) — built-in slot demos - [API Reference](/revturbine-sdk/api/) — `PlacementTypeRegistry` and `PlacementSlotProps` docs # Entitlements > Feature gates, usage limits, credits, seats, and capability tiers — check access and enforce limits with the SDK. Entitlements define what users can do based on their plan. The SDK evaluates entitlements locally from your Playbook in every mode, and provides React hooks and imperative APIs for access control. ## Entitlement Types | Type | Example | Check Pattern | |---|---|---| | **Feature gate** | `brand_kit`, `data_export` | Boolean — allowed or denied | | **Usage limit** | `api_calls`, `storage_gb` | Numeric — current vs. limit | | **Credits** | `render_credits` | Depleting balance | | **Seats** | `team_members` | Per-account count limit | | **Capability tier** | `analytics_tier` | Plan-level access (starter → pro → enterprise) | ## Checking Entitlements **One rule, every surface.** The verdict is `allowed` on an `EntitlementResult` — `rt.can()`, `useEntitlement().result`, the server re-check — and `can` on the `useCan` hook, which is `resolved && !denied` and so also covers the moment before the first answer arrives. `status` is the shape of the answer, never the verdict: `limited` appears on both sides of `allowed`. ### React Hook — `useCan` The simplest reactive check. `useCan(handle)` returns `{ can, limited, result }`. **`can` is the verdict**: `resolved && !denied`, where denied means `status === 'denied' || allowed === false`. A `limited` result that still grants (`allowed: true`) keeps `can` true; a `limited` result that denies is a deny. Gate on `!can` — it folds in "not yet resolved", which `result.allowed` alone cannot: ```tsx import { useCan } from "@revturbine/sdk"; import { DemoApp } from "./DemoApp"; function ExportButton() { const { can, limited } = useCan("data_export"); if (!can) return ; return ; } // Dan is not entitled to data_export, so `can` is false and the button is // disabled. Switch to user="user_alice" (entitled) and it enables. export default function App() { return ( ); } ``` ### Full surface — `useEntitlement` For the complete reactive result (`allowed` / `denied` / `limited` / `isLoading` / `error` / `gatedPlacement` / `recheck`), use `useEntitlement`: ```tsx import { useEntitlement } from '@revturbine/sdk'; function ExportButton() { const { allowed, denied, isLoading } = useEntitlement({ handle: 'data_export', }); if (isLoading) return ; if (denied) return ; return ; } ``` ### Declarative gate — `` `` gates its children declaratively. `can="handle"` checks an entitlement (shorthand for `check={{ entitlement: 'handle' }}`); children render while entitled, and a configured upgrade placement — or `deniedFallback` — shows on denial. `limitedFallback` renders a soft "running low" state while access is still granted: ```tsx 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 ( ); } ``` :::caution[`` must render inside a client boundary] In App-Router apps, `` (and every SDK component/hook) must be used **inside a `'use client'` component**. The SDK creates its React context at module scope, so importing it from a Server Component fails the build with `createContext is not a function`. Keep the page shell a Server Component if you like — but the subtree that renders `` needs the `'use client'` directive at its top. Server-side enforcement belongs to `checkEntitlement` on your API routes (see the client-vs-server section below), not to rendering `` on the server. ::: **A `` is also a placement.** On denial the gate doesn't render hardcoded UI — it resolves a **placement** from your monetization config and renders it in place of the children. So *what a denied user sees* — the upgrade modal or banner, its headline, offer, and CTA — is authored in the dashboard (or the CLI config), not baked into your app. Your PM, growth, or marketing team can rewrite the denial copy, swap the offer, or A/B-test it with no code deploy. `surfaceTemplateIds` scopes which surface templates can fill the gate; `deniedFallback` is the static backstop shown only when no placement is configured for it. #### Live gate example Switch demo users to see the gate respond — it runs in `local_only` mode with no backend. Two more gate scenarios are on the [Interactive Playground](/revturbine-sdk/playground/#access-gates): [Branding gate](/revturbine-sdk/playground/#gate-inline) and [Brand Kit gate](/revturbine-sdk/playground/#gate-card). **Data Export gate** — a blocking modal gate on a paid feature. _Live example: /playground/#gate-modal_ ### With Auto-Gate When `autoGate: true`, the hook automatically resolves an upgrade placement if the entitlement is denied: ```tsx const { allowed, denied, gatedPlacement } = useEntitlement({ handle: 'data_export', autoGate: true, gatePlacementRequest: { slotId: 'export_gate', surfaceTemplateIds: ['modal_overlay'], }, }); if (denied && gatedPlacement) { // Render gatedPlacement.content as an upgrade prompt } ``` ### Imperative API Pass only what your app knows better than the Playbook — current consumption, or the tier a feature needs. Everything else comes from plan configuration. | Entitlement type | Second argument | Result includes | |---|---|---| | Feature | — | `status`, `allowed` | | Capability tier | `{ requiredTier }` | `status`, `allowed`, `current_tier` | | Usage limit | `{ used }` | `status`, `allowed`, `limit`, `used`, `remaining` | | Credits | `{ balance }` | `status`, `allowed`, `remaining` | | Seats | — (counts come from the usage snapshot) | `status`, `allowed` | ```ts const result = await sdk.can('data_export'); // result.status: 'allowed' | 'limited' | 'denied' // result.allowed: boolean // result.reason: string (optional) // result.current_tier: string (optional, for tier-based) // result.limit / used / remaining: present when a usage_limit or credits rule matched ``` ## Entitlement Result Every entitlement check returns an `EntitlementResult`: ```ts interface EntitlementResult { status: 'allowed' | 'limited' | 'denied'; allowed: boolean; reason?: string; current_tier?: string; limit?: number; // effective limit from the matched rule used?: number; // amount the evaluation applied against limit remaining?: number; // max(0, limit - used) } // limit / used / remaining appear only on limit-bearing outcomes — a matched // usage_limit or credits rule. Their absence means the check was // limit-agnostic (a feature or tier rule), not unlimited. For balances across // handles, use useUsageSnapshot() (see "Reading Usage" below). ``` | Status | Meaning | |---|---| | `allowed` | Full access — proceed normally | | `limited` | At or past a limit. **Read `allowed`, not this** — under the default (unset) enforcement `limited` carries `allowed: false`; only `degrade` and `allow_overage` pair it with `allowed: true`. There is no "approaching" state. | | `denied` | No access — show upgrade prompt or block | ## Usage Tracking Report usage to keep entitlement checks accurate: ```ts // Update current balances sdk.update({ usage: { api_calls: 950, storage_gb: 45, team_members: 5, }, }); ``` `update()` triggers: - Re-evaluation of usage-based entitlements - Segment re-evaluation (targeting rules may change) - Decision cache invalidation - Local runtime state persistence `update()` accepts a widened session-context patch, so the same call can also refresh plan, traits, or page context. Usage balances always belong under the `usage` key; a bare entitlement handle is not interpreted as usage. ## Generate Entitlement Handle Types Generate a union from your Playbook so `can()` accepts only handles that exist: ```bash npx revturbine generate types ./revturbine.playbook.json --out src/revturbine.generated.ts ``` ```ts import { Entitlements } from './revturbine.generated'; // The generated value retains its EntitlementHandle literal type. const handle = Entitlements.feature.data_export; const result = await sdk.can(handle); ``` See the [CLI reference](/revturbine-sdk/reference/cli/#generate-typed-handles) for selectors and all supported generated type families. ### Reading Usage ```tsx import { useUsageSnapshot } from '@revturbine/sdk'; function UsagePanel() { const { usage, refresh } = useUsageSnapshot(); return (
    {Object.entries(usage).map(([handle, data]) => (
  • {handle}: {data.current} / {data.limit ?? '∞'}
  • ))}
); } ``` ## Patterns by Entitlement Type ### Feature Gate ```tsx const { allowed } = useEntitlement({ handle: 'brand_kit' }); // allowed: true/false ``` ### Usage Limit ```tsx const { allowed, limited, result } = useEntitlement({ handle: 'api_calls' }); // limited: at or past the limit — read `allowed` for the verdict // result.used / result.limit / result.remaining: on the result for this check ``` ### Credits ```tsx const { limited, result } = useEntitlement({ handle: 'render_credits' }); // result.status / result.allowed reflect access as credits run low // result.remaining: the balance the evaluation used ``` ### Seats ```tsx const { denied, result } = useEntitlement({ handle: 'team_members' }); if (denied) { // seat counts: read from useUsageSnapshot() (see "Reading Usage") // Show "seat limit reached" prompt } ``` ### Capability Tier ```tsx const { denied, result } = useEntitlement({ handle: 'advanced_analytics' }); if (denied) { // result.current_tier: 'professional' // Show "upgrade to Professional" prompt } ``` ## Fail-Closed Semantics A check grants access only when it can affirmatively justify the grant; every other outcome denies, with `allowed: false` and a `reason` naming why. That is what **fail-closed** means wherever these docs use it — and it is about the answer, not the network: the Playbook is cached locally, so a configured runtime answers without a round trip, and the no-rule cases below are the only ones where there was nothing to evaluate. The `reason` lets you tell those apart from a real denial: ```ts // Hosted mode, the launched Playbook could not be fetched: // { status: 'denied', allowed: false, reason: 'config_unavailable' } // Local mode, no Playbook and no cached result: // { status: 'denied', allowed: false, reason: 'entitlement_not_in_playbook' } // A real denial — a rule decided this: // { status: 'denied', allowed: false, reason: 'no_matching_entitlement_rule' } ``` See [Entitlement reason codes](/revturbine-sdk/guides/error-handling/#entitlement-reason-codes) for the complete emitted set. :::tip In `local_only` mode, all entitlement checks resolve instantly from the cached Playbook — no network calls. A configured Playbook gives real allow/deny answers, so the fail-closed fallback only fires when there's genuinely no config to evaluate against. ::: :::caution[`price_per_unit` and `rate_limit` are not gates] Those types carry metering and rate-limit parameters; they do not grant access. Calling `can()` on either returns `{ status: 'denied', allowed: false, reason: 'not_access_entitlement_kind' }` and reports a developer warning. Use an access entitlement to gate a feature, and read the parameters for your application's metering or rate limiter. Display tokens continue to expose the metadata. ::: ## Parameter entitlements `price_per_unit` and `rate_limit` configure application behavior; they are not always-allowed feature gates. `can()` returns `allowed: false`, `status: 'denied'`, and `reason: 'not_access_entitlement_kind'` for these kinds. Do not reinterpret that denial as a grant or use a parameter handle in `Gate`. Use a separate access entitlement for authorization. The application reads parameter metadata from its admitted Playbook/provider data and implements the meter or rate limiter itself. Resolve the appropriate rule for the current plan/target; do not simply take the first rule or infer unlimited access from an absent value. Display tokens can expose metadata for presentation, but a display token is not enforcement. This version does not provide a new universal `getParameter()` or `getRateLimit()` API. For example, inspect the configured rules through the initialized SDK's `session.sdk.getPlaybook()` accessor. This lists metadata for a handle; it deliberately does **not** select an effective rule for a particular user: ```ts import { initRevTurbine } from '@revturbine/sdk'; function inspectParameterRules( session: Awaited>, entitlementHandle: string, ) { const playbook = session.sdk.getPlaybook(); if (!playbook) throw new Error('No admitted Playbook is available'); return playbook.entitlement_rules .filter((rule) => rule.entitlement_id === entitlementHandle) .map((rule) => ({ targets: rule.targets, segment_ids: rule.segment_ids, rate_value: rule.rate_value, period_scope: rule.period_scope, amount_cents: rule.amount_cents, currency: rule.currency, })); } ``` Keep absent values absent. A rate value needs its period, and a price needs its currency; do not treat either as an access grant. Use this inspection for configuration/debugging or presentation. Application-owned enforcement must apply the intended plan, add-on, and segment policy rather than taking the first row returned here. If the admitted artifact does not include this metadata, use your application's authoritative configuration source instead of inventing a default. For mirrored access grants, `UserContext.entitlements` and `entitlementMerge` control how application facts combine with Playbook evaluation. The default gives application mirrors precedence. A result with `entitlement_mirrored` therefore need not have a Playbook `rule_handle`. See [SDK upgrades](/revturbine-sdk/guides/sdk-upgrades/) before copying old rule-only assertions. ## Next Steps - [Placements Guide](/revturbine-sdk/guides/placements/) — render upgrade prompts for denied entitlements - [Events & Analytics](/revturbine-sdk/guides/events/) — track entitlement check outcomes - [Error Handling](/revturbine-sdk/guides/error-handling/) — fail-closed checks and recovery patterns - [Try it live → Data Export Gate](/revturbine-sdk/guides/entitlements/#gate-modal) # Error Handling > SDK error model — what a placement does when it can't resolve, what an entitlement check returns when it can't justify a grant, the provider failure cascade, and graceful degradation patterns. The SDK never throws into your app and never blocks your render. But *placements* and *entitlement checks* degrade in opposite directions, on purpose: a placement that can't resolve **renders nothing**, while an entitlement check that can't resolve **denies**. ## Two degradation modes **A placement that can't resolve renders nothing** (or your configured fallback), so it can never take your product down. **Entitlement checks are [fail-closed](/revturbine-sdk/guides/entitlements/#fail-closed-semantics).** If a check can't produce an affirmative grant, it returns `{ status: 'denied', allowed: false }` rather than granting access. The Playbook is cached and persisted locally, so a configured runtime evaluates real allow/deny answers with no network round-trip; the failure fallback only fires when the SDK has *no basis to answer at all* — no config, no cache, nothing reachable — which is exactly where denying is the safe, non-leaking default. The `reason` code is preserved so you can still tell an infrastructure failure apart from a real denial. | API failure scenario | SDK behavior | |---|---| | Launched Playbook not fetched yet (hosted mode) | Placements return `visible: false` | | Entitlement check fails | Returns `{ status: 'denied', allowed: false, reason: 'config_unavailable' }` | | Config fetch fails | Falls back to cached Playbook; with none, entitlement checks deny | | Event delivery fails | Events are buffered and retried silently | ## Error Surface ### Hook Level Hooks expose errors as strings — they never throw: ```tsx const { error, isLoading } = usePlacement({ placement: { name: 'hero_banner' } }); const { error: entError } = useEntitlement({ handle: 'data_export' }); if (error) { // Non-critical — log and continue console.warn('Placement error:', error); } ``` ### Controller Level Headless controllers surface errors through state: ```ts const ctrl = new PlacementController(sdk, config); await ctrl.load(); if (ctrl.state.error) { console.warn('Controller error:', ctrl.state.error); } ``` ### SDK Level Most SDK methods fail silently and return sensible defaults: ```ts // Denies ({ allowed: false }) when it can't justify a grant await sdk.can('data_export'); // Silently drops event on delivery failure await sdk.track('page_viewed'); // Returns null decision on failure await sdk.getPlacement({ slotId: 'banner' }); ``` ## Provider Failure Behavior When the provider chain is exhausted (all providers failed), slots behave according to `providerFailureSlotBehavior`: ```tsx ``` | Value | Behavior | |---|---| | `'invisible'` | Slots render nothing — your layout stays intact | | `'placeholder'` | Slots render fallback placeholder content | ### Recommendation Use `'invisible'` (default) for production. Your app should work fine without any placement rendering. Use `'placeholder'` during development to visually verify that slots are wired correctly even when a provider fails. ## Reason Codes Placement decisions include `reasonCodes` that explain why a placement was hidden or shown. This is the complete set the SDK emits, generated from the public contract in `tests/reason-contract.json`. An interaction-flush failure and a client-side cap-enforcement fallback are each documented in their own place on the [Error Codes reference page](/revturbine-sdk/reference/errors/) instead of the table below, since neither is part of that contract. | Code | Meaning | |---|---| | `config_unavailable` | The Playbook could not be fetched, or a fetch is still in flight — the SDK has no config to decide against yet. | | `entitlement_gate_unmet` | The candidate's entitlement-gate trigger did not match the user's current entitlement state. | | `entitlement_not_denied` | The Access Gate placement's entitlement is not denied or limited for this user, so the gate does not fire. | | `gate_outside_access_gate` | An Access Gate placement was requested from a slot that is not an Access Gate slot; gates render only in gate slots. | | `no_eligible_candidate` | No candidate placement survived eligibility filtering for this slot. | | `no_gate_for_entitlement` | The Access Gate slot asks for an entitlement that no gate placement is authored for, so the slot shows its access-denied placeholder. | | `no_resolver_configured` | No placement resolver is configured, so the SDK has nothing to evaluate against. | | `placement_not_found` | No candidate output matched the requested placement id or name. | | `placement_not_registered` | The payload targets a placement handle the app never registered with the SDK. | | `placement_retired` | The candidate's rule is hidden — for example, superseded by a conversion — per impression history. | | `plan_target_mismatch` | The user's plan or billing period doesn't match the candidate's plan targeting. | | `qualifier_trigger_unmet` | The candidate's qualifier trigger condition was not satisfied for the user's plan or category. | | `sdk_disabled_provider_failure` | The SDK disabled itself after its configured providers failed, so it fails closed rather than risk showing something wrong. | | `segment_target_mismatch` | The user doesn't belong to any of the candidate's target segments. | | `suppressed_by_dismiss_cooldown` | The user dismissed this placement and its cooldown window has not elapsed. | | `suppressed_by_payload_cap_day` | The payload's per-day impression cap has been reached. | | `suppressed_by_payload_cap_lifetime` | The payload's lifetime impression cap has been reached. | | `suppressed_by_payload_cap_month` | The payload's per-month impression cap has been reached. | | `suppressed_by_payload_cap_session` | The payload's per-session impression cap has been reached. | | `suppressed_by_payload_cap_week` | The payload's per-week impression cap has been reached. | | `suppressed_by_payload_cooldown` | The payload is still inside its configured cooldown window since it was last shown. | | `suppressed_by_presentation_cap` | Every eligible candidate is over an overall presentation cap rule from Placement Settings. | | `suppressed_by_system_cooldown` | Every eligible candidate is a discretionary nudge inside the session cooldown since the last one shown. | | `suppressed_until_remind_window` | The user chose "remind me later" and that reminder window has not elapsed yet. | | `threshold_trigger_unmet` | The candidate's threshold trigger did not match the user's current entitlement usage. | | `trial_trigger_unmet` | The candidate's trial trigger did not match the user's trial state. | | `unknown_trigger_type` | The candidate uses a trigger type this SDK does not recognize, so it is not shown. | ### Inspecting Reason Codes ```tsx const { decision } = usePlacement({ placement: { name: 'hero_banner' } }); if (decision?.reasonCodes?.includes('suppressed_by_payload_cap_day')) { // User has hit this payload's daily impression cap } ``` ## Entitlement Reason Codes Entitlement checks are **fail-closed**. When the SDK cannot produce an affirmative grant — the Playbook never arrived, no rule grants the entitlement to the user's plan, the SDK was disabled — it returns `{ status: 'denied', allowed: false }` with a `reason` naming the cause. It never invents a grant. Treat `allowed` as the answer and `reason` as the explanation; the codes below are the complete set the SDK emits. ### Denied because a rule said so | Reason | Meaning | |---|---| | `no_matching_entitlement_rule` | No rule grants this entitlement to the user's plan. Plan targeting is explicit — an entitlement with no enabling rule for that plan is not granted. | | `feature_not_enabled_for_plan` | A matching `feature` rule has `enabled: false`. | | `usage_limit_reached` | The user is at or over a `usage_limit` rule's limit. | | `credit_balance_exhausted` | The user is at or over a `credits` rule's allowance. | `usage_limit_reached` and `credit_balance_exhausted` carry a suffix reflecting the rule's `enforcement` mode: | Suffix | `enforcement` | Outcome | |---|---|---| | *(none)* | `hard_block` | `denied` — hard stop | | *(none)* | unset | `limited`, **not** allowed | | `_block_with_upsell` | `block_with_upsell` | `denied` — render the upsell placement | | `_degraded` | `degrade` | `limited` but **allowed** (throttled, not blocked) | | `_overage` | `allow_overage` | `allowed` — metered overage | Two modes produce no suffix and they are **not** equivalent: `hard_block` denies, while leaving `enforcement` unset returns `limited` and does not allow. Branch on `allowed`, never on the presence of a suffix. :::note[Renamed in schema v15] `block_with_upsell` was previously called `soft_block`. The name was misleading — `soft_` reads as permissive, but the mode **denies**; it just tells you to render an upsell rather than a hard stop. Behaviour is unchanged, and the old spelling is still accepted when it appears in an already-compiled Playbook, so existing payloads keep deciding exactly as before. The reason code it produces is now `..._block_with_upsell` in every case. ::: ### Denied because the SDK could not decide | Reason | Meaning | |---|---| | `config_unavailable` | The launched Playbook could not be fetched (hosted mode). Not a rule decision — an infrastructure failure. | | `entitlement_not_in_playbook` | Local mode with no Playbook and no cached result: nothing describes this entitlement, so there is no basis to grant it. | | `sdk_disabled_provider_failure` | The SDK disabled itself after a provider failure. | ### Allowed with a reason | Reason | Meaning | |---|---| | `granted_by_reverse_trial` | Granted by an active reverse trial rather than by the user's plan. | :::note[Renamed in 0.3.0] `local_runtime_default_allow` is now `entitlement_not_in_playbook`. The old name stated a verdict the result does not have — it denies — and was also emitted by the headless runtime on an *allowed* result, so one code meant opposite things on two surfaces. There is no deprecated alias; update any `switch` on the old string. ::: :::caution[Two entitlement types are not gates] `price_per_unit` and `rate_limit` are parameter metadata, not access gates. Checking them as access entitlements returns a denied result with `not_access_entitlement_kind`. Read their configured values for presentation or application-owned enforcement; use a boolean or usage entitlement for an access decision. See [parameter entitlements](/revturbine-sdk/guides/entitlements/) for the boundary between metadata and access checks. ::: ## Retry Behavior | Operation | Retry Strategy | |---|---| | Placement resolution | No auto-retry. Call `refresh()` to retry manually. | | Entitlement check | No auto-retry. Call `recheck()` to retry manually. | | Event delivery | Auto-buffered and retried on next batch interval. | | Config fetch | Keeps the last-known-good Playbook; hosted mode re-checks for a new release about once a minute. | ### Manual Retry ```tsx const { refresh, error } = usePlacement({ placement: { name: 'hero_banner' } }); const { recheck, error: entError } = useEntitlement({ handle: 'data_export' }); // Retry after transient failure if (error) await refresh(); if (entError) await recheck(); ``` ## Design for the empty state Structure your components so the baseline UI never depends on a placement being present: ```tsx function Dashboard() { return (
{/* Baseline UX — always works */} {/* Placement — renders nothing when no placement matches */}
); } ``` When no placement matches the current user, `` renders nothing and the baseline dashboard is unchanged. Targeting, frequency caps and cooldowns can all produce that outcome, so the layout must hold up without it. ## Debugging Errors Enable verbose logging to diagnose issues: ```ts // In browser console localStorage.setItem('revturbine:debug', 'true'); ``` This logs decision resolution, provider chain evaluation, and error details to the browser console. ## Next Steps - [Configuration Reference](/revturbine-sdk/reference/configuration/) — error-related configuration options - [Error Codes Reference](/revturbine-sdk/reference/errors/) — enumerated error codes # Events & Analytics > Track user interactions, engagement, and placement lifecycle — automatically, declaratively, or by hand — with consent, redaction, and a typed event pipeline. RevTurbine reports what it did — a placement was shown, a CTA was clicked — and, when you opt in, what the user was doing around it. Placement lifecycle is tracked automatically; everything else is opt-in, additive, and privacy-safe by default. An integration that sets no telemetry options behaves exactly as before. Every event carries a sortable, unique `event_id`, is PII-redacted before it leaves the browser, and is batched and delivered best-effort (a failed send never throws into your app, and a retry resends the identical row so it can't duplicate). ## Automatic tracking Placement components and headless controllers instrument the placement lifecycle for you — you don't wire these up: | Event | When | |---|---| | `impression` | A placement is presented (see [viewport exposure](#viewport-exposure)) | | `placement_interaction` | The canonical interaction event, discriminated by `interaction_type`: `dismiss` · `remind_me_later` · `cta_clicked` · `cta_completed` · `suppress` | | `placement_rendered` / `placement_exposed` | The visual root rendered / entered the viewport (when you attach `exposureRef`) | `placement_interaction` is the one placement interaction event; there are no separate dismiss/snooze/convert events. ## Billing facts and application events With Stripe connected and identity mapping configured, RevTurbine records subscription, trial, renewal, and payment facts from Stripe webhooks. Your app does not need to emit a second billing fact merely because a checkout page returned successfully. A browser return URL is not authoritative payment proof. The billing event names are `subscription_started`, `subscription_renewed`, `subscription_expanded`, `subscription_canceled`, `trial_started`, `trial_converted`, `trial_expired`, `payment_succeeded`, and `payment_failed`. For example, record your app's checkout-button interaction as an application event; let the webhook integration report the resulting `payment_succeeded` and subscription fact. Do not send a duplicate `subscription_started` from the browser's checkout-success page. Use application events for what your app knows: feature use, an onboarding step, or an interaction that is not a Stripe billing occurrence. `track()` is the generic lane and namespaces collisions with platform event names. The typed `emitPlatformEvent` API is the platform-event lane and requires that event's payload contract; renaming an arbitrary `track()` event does not make it a canonical billing fact. `billing_ref` is a producer-owned stable billing-fact identity, not the SDK transport `event_id`. Preserve it when retrying the same fact. Do not reuse it for unrelated occurrences or invent a new one on each retry. Newer managed billing profiles distinguish occurrence identity, source revision, delivery ID, and the legacy billing reference; a legacy reference alone does not identify every repeated subscription transition. Prefer the Stripe integration for Stripe-owned facts rather than reproducing its accounting logic in the browser. The SDK does not emit the removed `growth_signal_observed` event. Use the current documented event/trigger contracts and keep custom names in the application lane. For identity mapping and webhook setup, see [Billing & Stripe](/revturbine-sdk/guides/billing/). ## Custom events In React, `useTrack` returns a `track(name, data?, options?)` bound to the surrounding [scope](#scoping-with-telemetryscope). Reserved names (canonical identity + provenance fields) are dropped so you can't overwrite a system value; without a provider, `track` is a safe no-op. ```tsx import { useTrack } from '@revturbine/sdk'; function AdvancedFilters() { const track = useTrack(); return ( ); } ``` Headless / server code uses `sdk.track`: ```ts await sdk.track('checkout_completed', { plan: 'professional', revenue: 99 }); ``` ### Track options | Option | Meaning | |---|---| | `area` / `action` | Scope labels; merge outer → inner → invocation | | `purpose` | **Advisory** intent (e.g. `'engagement'`). A server-side allowlist — not this value — decides whether an event feeds scoring | | `once` | Emit at most once for the hook's lifetime | | `dedupeKey` | Emit at most once per key | | `immediate` | Send now instead of batching | ## Scoping with TelemetryScope `TelemetryScope` sets ambient `area` / `action` / `purpose` for its descendants. It renders its children unchanged — no wrapper element, no added `role` — and nested scopes merge inner-over-outer, with per-event options winning. ```tsx import { TelemetryScope, useTrack } from '@revturbine/sdk'; {/* every useTrack() here inherits area="billing" */} ``` ## Declarative tracking | Component / hook | Fires | |---|---| | `TrackOnView` | Its event **once** when the element scrolls into view (falls back to render when `IntersectionObserver` is unavailable). Strict-Mode safe | | `EngagementArea` | A one-shot `engagement_view`, accrues `engagement_dwell` (`dwell_ms`) only while onscreen **and** the tab is visible, and bubbles descendant clicks as `engagement_interaction` | | `Track` (`asChild`) | Composes onto a child's `onClick` (no wrapper) — telemetry fires only if the child didn't `preventDefault`; leaves the child's accessible name and disabled state unchanged | | `useTelemetryProps` | Telemetry props to spread onto a primitive that can't take a wrapper | ```tsx import { TrackOnView, Track } from '@revturbine/sdk'; export const Examples = () => ( <> ); ``` ## Tracked & gated actions `useTrackedAction` wraps an async action, emitting `${name}_started` then `${name}_completed` or `${name}_failed` (with a non-sensitive `error_category` — never the raw message). It preserves the return value and re-throws, so it's a drop-in wrapper. ```tsx import { useTrackedAction } from '@revturbine/sdk'; const { run, isRunning } = useTrackedAction('export_pdf', () => exportPdf()); ``` `useGatedAction` is the React analog of `rt.gate(action, fn)`: it delegates to the SDK so it emits the same `gate_attempted` → `gate_allowed` / `gate_denied` sequence (not a fork), running the action wrapped in tracked-action telemetry only when allowed. A passively-rendered `` instead emits `gate_evaluated`. ## Consent `telemetry.consent` gates event **creation** ahead of every destination: | Value | Effect | |---|---| | `granted` (default) | Emit normally | | `denied` | No event is created — nothing reaches ingest, consumers, or integrations | | `pending` | Dropped in MVP; never persisted | Change it at runtime with no remount: ```ts sdk.setTelemetryConsent('denied'); // stop; 'granted' resumes on the next event ``` The keyless anonymous SDK-init beacon (`anonymousTelemetry`) is controlled separately and is unaffected by consent. ## Annotated DOM capture Opt into hand-authored capture with `domCapture` on the provider. One delegated listener per event reads **only** allowlisted `data-rt-*` attributes — never element text, input values, `href`s, or selectors — and always skips password / file / hidden / payment-autocomplete controls. A `data-rt-no-capture` ancestor opts an element and its subtree out. Collected values pass through the same PII redactor. ```tsx ``` ## Viewport exposure `placementExposure` controls when the presentation-writing `impression` fires: | Mode | Impression fires | |---|---| | `legacy_resolution` (default) | At decision resolution — exactly as today | | `render` | When the placement renders | | `viewport` | When it scrolls into the viewport; falls back to resolution (`exposure_basis: 'render_fallback'`) when `IntersectionObserver` is unavailable | `usePlacement` returns `exposureRef`; slot components receive the same ref through the additive `exposureRef` prop on `PlacementSlotProps`. The renderer threads it to every slot automatically, and a **custom component** must attach it to its true visual root. The built-in slots do not consume the ref, so their viewport mode uses the render fallback. :::caution[Metric-migration note] Only `placementExposure: 'viewport'` **with `IntersectionObserver` present** moves the `placement_presentations` denominator to viewport-qualified presentations — a deliberate metric-definition change. CTR and conversion-rate for a placement switched to `viewport` reflect *seen* presentations from that point forward and no longer compare against pre-switch history. `legacy_resolution` (the default) leaves the denominator — and every existing dashboard — unchanged. ::: ## Third-party analytics Forward the same semantic events to a third-party tool without adding a second transport. `createPostHogIntegration` adds an opt-in identity lifecycle (all sync flags default `false`, so it behaves like the capture-only provider until you turn one on); PostHog is injected, never bundled. ```ts import posthog from 'posthog-js'; import { createPostHogIntegration } from '@revturbine/sdk'; initRevTurbine({ domainProviders: [createPostHogIntegration({ posthog, syncIdentity: true })], // ... }); ``` A throwing integration never blocks RevTurbine ingest, and an ingest failure never blocks a configured mirror. ## Trigger events Trigger events are semantic lifecycle signals the SDK uses to re-evaluate targeting. Emit one and all active placements re-evaluate: ```ts await sdk.emitTrigger('trial_expiring', { days_remaining: 2 }); await sdk.emitTrigger('usage_limit_approaching', { entitlement_handle: 'api_calls', usage_percent: 95, }); ``` Built-in triggers: `trial_midpoint` · `trial_expiring` · `trial_expired` · `usage_limit_approaching` · `usage_limit_reached` · `credit_balance_low` · `seat_limit_reached` · `feature_gated` · `cancel_intent` · `payment_failed` · `auto_renewal_reminder` · `onboarding_complete` · `invite_teammate_prompt` · `referral_offer` · `plan_upgrade_nudge`. The compiler rejects any other name. :::tip Triggers are the primary way to activate message-type placements (toasts, banners, modals). Dashboard targeting rules reference them to decide when to show a placement. ::: ## Marking test traffic Set `test: true` at init when an SDK instance generates **test traffic** — a staging deploy, an integration test, a QA walkthrough: ```ts import { initRevTurbine } from '@revturbine/sdk'; const rt = await initRevTurbine({ publicKey: process.env.NEXT_PUBLIC_REVTURBINE_PUBLIC_KEY!, // public key — safe in the browser test: true, // this instance's events are test traffic }); ``` Every emitted event (clickstream and treatment interactions) is then stamped `test: true`, and RevTurbine analytics **exclude those events by default** from every metric, rollup, and denominator. An `include_test=1` toggle on the analytics pipes reveals them when you want to inspect test traffic. This is a deliberate, code-passed decision: pass it explicitly from your own configuration. The SDK never infers it from the environment — no `NODE_ENV` sniffing. Whether traffic is test traffic is your call, not a deployment artifact. Omitted or `false`, events are emitted unchanged (byte-identical to earlier SDK versions). :::note[Not the same as placement "Test Mode"] Placement **Test Mode** (`off / test_users / all_traffic` on a placement's settings) controls **who is eligible to see** a placement during SDK evaluation and carries its own decision marker. The `test` init option is a *caller-declared* marker on **emitted events**. A production user in a placement's test audience is not test traffic; a staging deploy emitting real-looking events is. The two are independent. ::: ## Silent-failure diagnostics When a placement or entitlement can't resolve for **infrastructure** reasons, the SDK reports it instead of failing silently — a `resolution_failure` diagnostic on the anonymous meta channel: | Reason | Emitted when | |---|---| | `placement_not_registered` | A decision is requested for a placement the app never registered — the classic "my payload renders nothing, silently" | | `config_unavailable` | The Playbook never arrived, so a placement fell back or an entitlement check denied without a rule saying no | | `sdk_disabled_provider_failure` | The provider chain failed: entitlement checks deny with this reason and slots follow `providerFailureSlotBehavior` | Diagnostics carry **only** author-defined handles (placement, slot, plan, entitlement) and closed reason codes — never user context, free text, or your tenant identifier (deployments are counted through a one-way hash). They're deduplicated per session and capped, fire from keyed production installs as well as keyless ones, and are silenced by **either** opt-out: ```ts initRevTurbine({ analytics: false }); // silences diagnostics too initRevTurbine({ anonymousTelemetry: false }); // likewise ``` Rule-based denials and local mode's default behavior are not failures and never emit. ## Your taxonomy in analytics Events that carry the `area` / `action` / `purpose` taxonomy (from `TelemetryScope`, `useTrack` options, `EngagementArea`, and tracked actions) are additionally stored with the taxonomy as first-class analytics columns — so engagement queries group by area and action directly, with no JSON unpacking and no configuration on your side. Untagged events are unaffected. ## Privacy & delivery - **Redaction.** Email- and card-shaped values in properties and traits are scrubbed to `[REDACTED]` before any destination — one sanitized envelope, every mirror. Best-effort, not a guarantee: don't put PII in event data. - **Advisory purpose, server-owned scoring.** A client-declared `purpose` never grants engagement eligibility; a static server-side allowlist decides which event names feed scoring, regardless of purpose. - **Delivery.** Events batch and flush on size, interval, or page-unload. A transient failure is retried with the byte-identical row, so storage collapses it to one; a permanent failure is dropped silently. In `local_only` mode nothing is sent at all — the right mode for demos and examples. ## Next steps - [Placements Guide](/revturbine-sdk/guides/placements/) — placement lifecycle and interactions - [Runtime Modes](/revturbine-sdk/guides/runtime-modes/) — local vs hosted mode - [Error Handling](/revturbine-sdk/guides/error-handling/) — graceful event-delivery failures # Run an Experiment > Let RevTurbine assign variants natively, or bring the assignment your GrowthBook or Optimizely client already made — either way, decisions, placements and telemetry read the same assignment. RevTurbine can allocate a native experiment or consume the assignment made by the experimentation SDK already running in your application. For an external assignment, your application owns and initializes the vendor client. A small RevTurbine domain provider maps the vendor's keys to canonical RevTurbine experiment and variant handles. The normalized assignment becomes part of an ephemeral effective user context. Segments, entitlements, placements, direct variant reads, and telemetry all use that same resolved assignment. RevTurbine never persists the vendor client or its credentials and never mutates the user object supplied by your app. **Note — Client providers and data connections are different:** Configure assignment adapters in application code. **Experiment Data Connections** in the RevTurbine dashboard are server-side connections for evidence or analysis; they do not install or configure a browser assignment SDK. ## The flow 1. In the experiment editor, choose the assignment source: **native** (RevTurbine assigns) or **Customer SDK provider** (your vendor SDK assigns). 2. Native: register the built-in bucketer (next section). Vendor: initialize the vendor SDK as usual and create a RevTurbine adapter with explicit experiment and variant mappings. 3. Pass the provider in `domainProviders` when initializing RevTurbine. 4. Use `getExperimentVariant()` when application code needs the selected variant directly; RevTurbine decisions and telemetry already use the same assignment. ## Native allocation No vendor SDK needed. Register the built-in bucketer as a domain provider. It assigns deterministically per subject, so the same user lands in the same arm on every load, and everything downstream — targeting, placements, `getExperimentVariant()`, telemetry — reads that one assignment. ```ts import { initRevTurbine, createBasicExperimentProvider } from '@revturbine/sdk'; const rt = initRevTurbine({ tenantId: 'tenant_abc', apiKey: 'rtk_…', // publishable ingest key endpoint: 'https://revturbine.com/app', mode: 'snippet', domainProviders: [ createBasicExperimentProvider({ subject: () => currentUserId, // re-read on every resolve experiments: { pricing_test: { variants: { control: 1, treatment: 1 } }, // relative weights onboarding_copy: { variants: ['a', 'b'], exposure: 0.2 }, // 20% enrolled }, }), ], }); ``` A subject outside `exposure` is **not enrolled** — the experiment is simply absent from their assignment, which is different from being in control. No stable subject, no assignment: bucketing an anonymous id would hand the same person a different arm on the next load. An allocation authored in the dashboard — an Experiment version with its handle, traffic allocation and weighted variants — drives `createNativeExperimentAssignmentProvider` instead, which also emits the assignment fact that ties results back to that version. See the API reference for its options. ## GrowthBook The GrowthBook adapter accepts an initialized customer-owned client. It is a separate optional entry, so GrowthBook is not included when your application imports only the base SDK. ```ts import { GrowthBook } from '@growthbook/growthbook'; import { initRevTurbine } from '@revturbine/sdk'; import { createGrowthBookExperimentAssignmentProvider } from '@revturbine/sdk/growthbook'; const userId = 'user_123'; const growthbook = new GrowthBook({ attributes: { id: userId }, }); await growthbook.init({ apiHost: 'https://cdn.growthbook.io', clientKey: 'sdk-...', }); const experimentProvider = createGrowthBookExperimentAssignmentProvider({ client: growthbook, bindings: [{ experimentHandle: 'pricing_test', featureKey: 'pricing-variant', allowedVariants: ['control', 'treatment'], }], }); const rt = initRevTurbine({ tenantId: 'tenant_abc', publicKey: 'rtk_…', // public key — safe in the browser user: { id: userId, plan_handle: 'free' }, domainProviders: [experimentProvider], }); ``` Keep the GrowthBook attributes current using the GrowthBook client. When the user changes in RevTurbine, `identify()` invalidates the effective context and resolves the provider for the new identity. ## Optimizely The Optimizely adapter also accepts an initialized client and derives the Optimizely user id from the current RevTurbine context by default. ```ts import { createInstance } from '@optimizely/optimizely-sdk'; import { initRevTurbine } from '@revturbine/sdk'; import { createOptimizelyExperimentAssignmentProvider } from '@revturbine/sdk/optimizely'; const optimizely = createInstance({ sdkKey: 'your-optimizely-sdk-key' }); await optimizely.onReady(); const experimentProvider = createOptimizelyExperimentAssignmentProvider({ client: optimizely, bindings: [{ experimentHandle: 'pricing_test', experimentKey: 'pricing-experiment', allowedVariants: ['control', 'treatment'], }], attributes: ({ userContext }) => ({ plan: userContext.plan_handle, }), }); const rt = initRevTurbine({ tenantId: 'tenant_abc', publicKey: 'rtk_…', // public key — safe in the browser user: { id: 'user_123', plan_handle: 'free' }, domainProviders: [experimentProvider], }); ``` Mappings are explicit: vendor keys do not leak into Playbooks. An allowed variant list prevents a misspelled or newly introduced vendor value from being silently treated as a valid RevTurbine variant. ## Read the selected variant Use the structured selection when your application must render variant-specific behavior itself: ```ts // `sdk` is the SDK instance: `useRevTurbine().sdk` in React, `session.sdk` headless. const selection = await sdk.getExperimentVariant('pricing_test'); if (selection.status === 'assigned') { renderPricingVariant(selection.variantHandle); } else { // Missing, unavailable, conflicting, and unsupported assignments are never // fabricated as "control". Keep the existing safe experience. renderDefaultPricing(); } ``` The result distinguishes `assigned`, `not_assigned`, `unavailable`, and `unsupported`. This lets you keep non-enrollment separate from vendor failure and diagnose ownership or assignment conflicts without guessing. For diagnostics, the same immutable context used by local evaluation is available from `getEffectiveUserContext()`: ```ts const effectiveContext = await sdk.getEffectiveUserContext(); console.log(effectiveContext.experiments?.pricing_test); ``` ## Supply an assignment directly An adapter is optional when your code has already normalized the assignment. Use the canonical `experiments` map, not an arbitrary custom trait: ```ts import { initRevTurbine } from '@revturbine/sdk'; const rt = await initRevTurbine({ publicKey: process.env.NEXT_PUBLIC_REVTURBINE_PUBLIC_KEY!, // public key — safe in the browser user: { id: 'user_123', plan_handle: 'free', experiments: { pricing_test: 'treatment' }, }, }); ``` Direct and provider assignments coalesce when they agree. If they disagree, RevTurbine reports an assignment conflict and does not invent a winner. Two providers also cannot own the same experiment handle. ## Target decisions and measure outcomes Create a Segment for the experiment in the RevTurbine dashboard (Targeting Studio) and reference it from an entitlement rule or placement payload. The SDK resolves the provider before evaluating the Playbook, so experiment-backed Segment membership and the resulting placement or entitlement use the effective assignment. Track the conversion normally: ```ts await rt.track('upgrade_completed', { plan: 'professional', source: 'pricing_experiment', }); ``` The SDK stamps the effective experiment assignment on telemetry from the same resolution used by direct selection and local decisions. It does not call the vendor separately for each consumer. ## Migrating from custom traits If an existing integration stores a variant under `custom.pricing_experiment`, move it to either a client provider binding or `experiments.pricing_test`. Update the Segment to use the canonical experiment handle, validate the vendor's variant vocabulary, and then remove the old custom-trait predicate. Do not configure an Experiment Data Connection for client assignment. ## Related - [Placements](/revturbine-sdk/guides/placements/) — how placement payloads, targeting, and priority resolve - [Entitlements](/revturbine-sdk/guides/entitlements/) — entitlement rules and precedence - [Events & Analytics](/revturbine-sdk/guides/events/) — signals used to measure the experiment # Headless API > Use the RevTurbine SDK without React — from vanilla JS, Vue, Svelte, Angular, or server-side code. 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 | ```ts import { initRevTurbine, PlacementController, EntitlementGate, } from '@revturbine/sdk/headless'; ``` ## The headless contract Headless is intended for **entitlement checks and user-context updates** — `session.can()`, `session.update()`, usage reporting, event tracking. Two rules follow from that: 1. **You supply the inputs.** Pass a `user` context at init, and in local mode pass the Playbook (`localRuntime: { playbook }`). There is no React provider doing this for you. 2. **Placements need an explicit slot.** To resolve a placement headlessly you provide a slot id **and** the surface template ids it accepts (`surfaceSlot: { id, surfaceTemplateIds }`). The React entry's automatic gate-placement resolution (``) is a React affordance — in headless code, denied-state upsell surfaces resolve through the slot config you declare. ## Quick Start ```ts import { initRevTurbine } from '@revturbine/sdk/headless'; import playbook from './revturbine.playbook.json'; // Local mode — no account, no network: user + Playbook are the inputs. const session = await initRevTurbine({ runtimeMode: 'local_only', localRuntime: { playbook }, user: { id: 'user_123', plan_handle: 'pro' }, }); // Hosted mode — swap the local runtime for your tenant credentials: // const session = await initRevTurbine({ // tenantId: 'tenant_abc', // publicKey: 'rtk_…', // public key — safe in the browser // user: { id: 'user_123', plan_handle: 'pro' }, // }); // Entitlement — the primary headless verbs const result = await session.can('brand_kit'); if (!result.allowed) { console.log('Denied:', result.reason); } session.update({ usage: { api_calls: 150 } }); // Placement — explicit slot id + the surface template ids it accepts const banner = session.placement({ surfaceSlot: { id: 'upsell_banner', surfaceTemplateIds: ['general_banner'] }, }); await banner.load(); if (banner.visible) { console.log('Show banner:', banner.content); } // Entitlement gate with a denied-state surface const gate = session.entitlement({ handle: 'brand_kit', autoGate: true }); await gate.check(); if (gate.denied) { console.log('Show upgrade gate:', gate.gatedPlacement); } ``` ## `initRevTurbine(options)` Creates a fully-initialized session. Handles SDK initialization, user identification, theme resolution, and optional placement bootstrapping. ```ts import { initRevTurbine, RuntimeMode } from '@revturbine/sdk/headless'; const session = await initRevTurbine({ publicKey: process.env.NEXT_PUBLIC_REVTURBINE_PUBLIC_KEY!, // public key — safe in the browser user: { id: 'user_123', plan_handle: 'pro' }, runtimeMode: RuntimeMode.Server, bootstrapPlacements: [ { placement: { name: 'pricing_banner' } }, ], }); ``` ### Options 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`, `publicKey`, `endpoint`, `runtimeMode`, `user`, `localRuntime`, `provider`, `providerFallbacks`, etc. ## `SdkSession` The session returned by `initRevTurbine()`. Provides the full imperative API. ### User Context ```ts session.identify('user_456', { plan_handle: 'enterprise' }); session.setUserContext({ personalization: { company: 'Acme' } }); const ctx = session.getUserContext(); session.resetIdentity(); // update() patches any context field except identity `id`. Usage goes under // the `usage` key — a bare handle is not a context field and is dropped with // a dev warning. session.update({ usage: { api_calls: 150 } }); session.update({ plan_handle: 'enterprise', plan: { handle: 'enterprise', name: 'Enterprise' } }); const fullCtx = await session.fetchUserContext('user_456'); const trial = await session.getTrialStatus(); const usage = session.getUsage(); ``` ### Factory Methods ```ts const ctrl = session.placement({ surfaceSlot: { id: 'upsell_banner', surfaceTemplateIds: ['general_banner'] }, }); const gate = session.entitlement({ handle: 'brand_kit', autoGate: true }); const decision = await session.getPlacementBySlotId('upsell_banner'); const result = await session.can('brand_kit'); await session.track('feature_used', { feature: 'export_csv' }); // Short-lived process (script, job, edge handler)? Flush before exit so // buffered events aren't lost: await session.flushEvents(); ``` ## `PlacementController` Manages the full placement lifecycle: register → decide → impression-track → interact. ### Basic Usage ```ts const ctrl = session.placement({ surfaceSlot: { id: 'pricing_banner' }, }); await ctrl.load(); if (ctrl.visible) { renderBanner(ctrl.content); } ``` ### Options | Option | Type | Default | Description | |---|---|---|---| | `surfaceSlot` | `{id, name?, surfaceTemplateIds}` | — | Canonical surface slot (preferred). Headless resolution needs the slot id AND the surface template ids it accepts | | `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` | — | Custom traits for decision request | | `ttlMs` | `number` | — | Decision cache TTL | | `autoTrackImpression` | `boolean` | `true` | Auto-record impression on visible decision | | `route` | `string \| (() => string \| null \| undefined)` | — | App route this slot renders on, stamped onto slot lifecycle events (a string or a getter read at emission time). Normalized like the React path; omitted, headless emits `route: null`. See [Route surface](/revturbine-sdk/guides/placement-cookbook/#route-surface) | ### State & Getters ```ts 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 ``` ### Interactions ```ts 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 ``` ### Reactive Updates ```ts const unsub = ctrl.onChange(() => { console.log('State changed:', ctrl.state); updateUI(ctrl.state); }); unsub(); // stop listening ``` ## `EntitlementGate` Checks feature access and optionally resolves a gated placement for denied users. ### Basic Usage ```ts 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); } ``` ### Options | 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 | ### State & Getters ```ts 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 ``` ### Reactive Updates ```ts const unsub = gate.onChange(() => { updateAccessUI(gate.state); }); await gate.recheck(); // re-run the check ``` ## Framework Integration Patterns **Vanilla JS** ```ts 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(); }); ``` **Vue 3** ```ts import { ref, onMounted, onUnmounted } from 'vue'; import { initRevTurbine } from '@revturbine/sdk/headless'; export function usePlacement(slotId: string) { const state = ref({ visible: false, content: null }); let unsub: (() => void) | undefined; onMounted(async () => { const session = await initRevTurbine(options); // your init options const ctrl = session.placement({ surfaceSlot: { id: slotId } }); unsub = ctrl.onChange(() => { state.value = { visible: ctrl.visible, content: ctrl.content }; }); await ctrl.load(); }); onUnmounted(() => unsub?.()); return state; } ``` **Svelte** ```ts import { writable } from 'svelte/store'; import { initRevTurbine } from '@revturbine/sdk/headless'; export function createPlacementStore(slotId: string) { const store = writable({ visible: false, content: null }); (async () => { const session = await initRevTurbine(options); // your init options const ctrl = session.placement({ surfaceSlot: { id: slotId } }); ctrl.onChange(() => { store.set({ visible: ctrl.visible, content: ctrl.content }); }); await ctrl.load(); })(); return store; } ``` ## Comparison: Headless vs React | 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. ## Live example Runs in `local_only` mode with no backend. Two more headless scenarios are on the [Interactive Playground](/revturbine-sdk/playground/#headless-api): [PlacementController](/revturbine-sdk/playground/#headless-placement) and [SdkSession one-shot](/revturbine-sdk/playground/#headless-session). **EntitlementGate** — an entitlement check without React. _Live example: /playground/#headless-gate_ # Visitor, User, and Account Identity > Handle signed-out visitors, sign-in, sign-out, and account changes without carrying another user's grants. A **visitor** is not yet signed in. A **user** is a person identified by your application's stable internal ID. An **account** is the organization or billing scope that person belongs to. These identifiers are different: use `user.id` for the person and `account_id` for their current account. An account ID is not a replacement user ID, and a Stripe payer may map to an account separately. ## Before sign-in Omit `user` intentionally for a signed-out visitor. The SDK uses anonymous identity for attribution; anonymous identity is not authentication or proof of a paid plan. Supplying a user object with a blank ID logs an error and falls back to anonymous identity. Do not use a blank string as your loading state. If the app needs a stable guest identity across its own sessions or devices, the app owns that identity and its authentication policy. The SDK's anonymous storage is not an account system. Do not assume it persists across server requests, cleared storage, or isolated browser contexts. ## Sign-in and user switches Resolve the authenticated user's ID and authoritative account/plan before granting paid access. Use `identify` when the identity changes, and `update` for changing facts about the same user. Use a stable internal ID, never an email address; email can change and is personal data. An anonymous visitor becoming an identified user retains applicable accumulated state. Switching from one identified user to a different identified user clears the previous user's context, usage, trial state, and application-provider snapshot before applying the new user's facts. Do not rely on a user switch to load facts your application has not supplied. For sign-out, call the SDK's `resetIdentity()` and clear your application's own authenticated state. Then return to the visitor experience. Clear private application caches and cancel in-flight user work as part of your app's logout; the SDK cannot clear application state it does not own. ## Account changes The same person may switch accounts without changing their user ID. Refresh the complete account-scoped context from your trusted backend, including plan, usage, add-ons, and mirrored grants. Explicitly clear obsolete values rather than merging a new account ID onto the old account's entitlements. On a server, construct a fresh session for the selected user/account operation. An account membership check belongs to your application. A context token should be issued only after that check; never sign an arbitrary client-provided `account_id` or plan. See [cloud enforcement](/revturbine-sdk/guides/cloud-entitlements/). ## Lifecycle test Start signed out, sign in as a paid user, sign out, then sign in as a free user without reloading the application. Assert the actual SDK context and gate result after every transition. Repeat with one user switching accounts. The free user must not retain the paid user's grants or balances. # Replace Hard-Coded Plan Checks > Watch a single `if (user.plan === 'pro')` grow into a tangle of business exceptions — beta overrides, comps, segmented copy, usage limits, grandfathered plans — then collapse it to one gate() call plus config. You already have a check like `if (user.plan === 'pro')` somewhere. It works — until the requirements arrive. Here's that one check growing up, and where it lands. ## The check that grows ### 1. The naive check ```tsx function ExportButton({ user }) { if (user.plan !== 'pro') return ; return ; } ``` Fine on day one. ### 2. …plus a beta list and a comp window Sales comped a few accounts; beta users get everything: ```tsx const BETA_ACCOUNTS = ['acct_123', 'acct_456']; function ExportButton({ user }) { const entitled = user.plan === 'pro' || BETA_ACCOUNTS.includes(user.accountId) || (user.compedUntil && user.compedUntil > Date.now()); if (!entitled) return ; return ; } ``` ### 3. …plus segmented upgrade copy Marketing wants different messaging by plan cadence and region: ```tsx function ExportButton({ user }) { const entitled = user.plan === 'pro' || BETA_ACCOUNTS.includes(user.accountId) || (user.compedUntil && user.compedUntil > Date.now()); if (!entitled) { const message = user.billing === 'annual' ? 'Unlock exports on your annual plan' : user.region === 'EU' ? 'Passez au plan Pro pour exporter' : 'Unlock exports — upgrade to Pro'; return ; } return ; } ``` ### 4. …plus a usage limit and a grandfathered plan Free users get 3 exports a month; the retired `starter_2022` plan keeps unlimited: ```tsx function ExportButton({ user }) { const isLegacy = user.plan === 'starter_2022'; const entitled = user.plan === 'pro' || isLegacy || BETA_ACCOUNTS.includes(user.accountId) || (user.compedUntil && user.compedUntil > Date.now()); const overLimit = !entitled && user.exportsThisMonth >= 3; // ...and now the copy branch has to know about isLegacy and overLimit too. } ``` By step 4 it's a knot of business exceptions living in your component — and changing the beta list, the copy, or the limit is a code change, a deploy, and an eng ticket. ## The after ```tsx import { Gate } from '@revturbine/sdk'; function ExportButton() { return ( ); } ``` The beta list, the comp window, the segmented copy, the usage limit, and the grandfathered plan all move into the **Playbook** — entitlement rules, segments, and the placement copy. Your component asks one question: *can this user export?* ## Still your job: the server check and the CTA The `after` is not a magic one-liner — your app still owns two things: ```ts // 1. The money-safe decision, on your server, before the paid work runs — // the same SDK, evaluating the same Playbook in-process. import { initRevTurbine, RuntimeMode } from '@revturbine/sdk/headless'; import playbook from './revturbine.playbook.json'; export async function POST(request: Request): Promise { const user = await getSignedInUser(request); // your own auth const rt = await initRevTurbine({ runtimeMode: RuntimeMode.LocalOnly, localRuntime: { playbook }, // loaded once per process user: { id: user.id, plan_handle: user.planHandle }, }); const result = await rt.can('data_export'); if (!result.allowed) return Response.json({ error: 'upgrade_required' }, { status: 402 }); return runExport(request); } ``` ```tsx // 2. The upgrade navigation, wired to the placement the renders when denied. router.push(path.url ?? '/pricing')}> ``` See [Client vs Server Enforcement](/revturbine-sdk/concepts/enforcement/) for why the server check stays. ## The real difference **Tip — It's not about line count:** In the **before**, changing the beta list or the upgrade copy is a code change + deploy + eng ticket, and your app has to *know about plans*. In the **after**, your app knows only **slots and entitlements** — a PM edits the targeting and copy in the dashboard, and nothing ships. The first monetization strategy is never the last; this is what keeps your code from changing every time it moves. ## Related - [Recommended API Path](/revturbine-sdk/getting-started/api-overview/) — `can()` vs `gate()` vs `` - [Entitlements](/revturbine-sdk/guides/entitlements/) — feature / usage-limit / credits / seats rules - [Run an Experiment with Segments](/revturbine-sdk/guides/experiments/) — the segmented-copy branch, done in config # Paywall Host Pattern > One always-mounted slot, a paywall store, and a 402 to slot map — the reference architecture for gating a whole app. Most apps need a paywall in more than one place: a feature button, a quota ceiling hit mid-action, a server rejecting a request. The obvious approach — mount a slot everywhere a paywall might appear — scatters placement logic across the codebase and makes it very hard to answer the one question that matters when something doesn't show up: *is this slot mounted right now?* The paywall host pattern inverts that. **One slot, mounted once, always.** Every other part of the app asks a store to open it. ## The three pieces ### 1. One always-mounted slot Mount a single `FixedSurfaceSlot` — the fixed-category variant of [``](/revturbine-sdk/guides/placements/#slot-types), with an `onDismissed` callback — high in the tree: in your root layout, above the router, so it is mounted on every route. ```tsx // app/layout.tsx import { FixedSurfaceSlot } from '@revturbine/sdk'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} {/* Mounted once, on every route. Nothing else in the app mounts a paywall. */} ); } ``` Because it is always mounted, "the slot wasn't there" stops being a possible explanation for a paywall that didn't appear. That removes an entire class of debugging. ### 2. A paywall store The store holds *why* the paywall should be open, not *what* to render — the content comes from the Playbook. The store below uses only React — no state library — because the one property that matters is awkward to get from a hook alone: it must be **settable from outside React**, so a `fetch` wrapper can open the paywall. Zustand, Redux, signals or Context all work; swap this for whatever your app already uses. ```tsx // lib/use-paywall.ts import { useSyncExternalStore } from 'react'; let blockedEntitlement: string | null = null; const listeners = new Set<() => void>(); /** Imperative half — callable from anywhere, including non-React code. */ export const paywall = { open(entitlementHandle: string): void { blockedEntitlement = entitlementHandle; listeners.forEach((listener) => listener()); }, close(): void { blockedEntitlement = null; listeners.forEach((listener) => listener()); }, }; /** Reactive half — the entitlement the user bumped into, or null when closed. */ export function usePaywall(): string | null { return useSyncExternalStore( (listener) => { listeners.add(listener); return () => { listeners.delete(listener); }; }, () => blockedEntitlement, // Server snapshot: nothing is open during SSR. () => null, ); } ``` Any component can now request a paywall without knowing anything about placements: ```tsx async function handleExport() { const result = await sdk.can('feature_export'); if (!result.allowed) { paywall.open('feature_export'); return; } await runExport(); } ``` ### 3. A 402 → slot map Client-side checks cover the cases the client knows about. The server is the authority, and it will sometimes reject a request the client thought was fine — a quota consumed on another device, a subscription that lapsed mid-session, a race. Map that rejection back into the same paywall. Entitlement checks fail **closed**, so a check that cannot complete denies rather than grants. The server rejection and the client check agree on direction. ```ts // lib/api-client.ts import { paywall } from './use-paywall'; /** Which entitlement a 402 refers to, by the header the API sends back. */ const ENTITLEMENT_BY_CODE: Record = { export_limit: 'feature_export', seat_limit: 'feature_seats', ai_credits: 'feature_ai_generation', }; export async function apiFetch(input: RequestInfo, init?: RequestInit) { const response = await fetch(input, init); if (response.status === 402) { const code = response.headers.get('x-entitlement-code') ?? ''; const handle = ENTITLEMENT_BY_CODE[code]; if (handle) { paywall.open(handle); } // Fall through: the caller still sees a failed response and can render its // own inline state. The paywall opens in addition to, not instead of, the // caller's own error handling. } return response; } ``` Keep the map explicit. A `402` with an unrecognized code should **not** silently open a generic paywall — you will never find out the code was wrong, and the user gets a paywall for something they can already do. ## Handling dismissal A dismissal is not the same as "nothing matched". `FixedSurfaceSlot` renders its `fallback` when no placement matches — correct for an always-present slot — and renders nothing at all once the user dismisses. Use `onDismissed` to clear your store so the app's own state agrees with what is on screen: ```tsx paywall.close()} /> ``` Without this the store still believes a paywall is open after the user closed it, and the next `open()` for the same entitlement looks like a no-op. Dismissal is also remembered by the SDK: a dismissed placement stays suppressed for its authored cooldown, and a **converted** one is retired permanently. Your store does not need to track either — asking again is safe, and the SDK answers. ## Why a config-side audit cannot verify this This is the part worth internalising, because we got it wrong ourselves. When an integration reported that its paywalls never appeared, we audited the Playbook: the placements existed, the entitlements existed, the targeting was right, and the fixed slots were configured. We concluded the slots were not mounted. **They were mounted.** The call sites passed slot ids that no placement targeted — a slot id typo on one side and a stale slot id on the other. The config was fine and the mounting was fine; the two simply did not refer to the same slots. A config-side audit **cannot see call sites**. It can tell you a placement targets `slot_paywall`; it cannot tell you your code mounts `slot_paywall_v2`. Both directions fail silently and they fail differently: | situation | what you observe | |---|---| | A placement targets a slot no code mounts | **Nothing at all.** The decision path is never asked about a slot that was never mounted, so there is no error, no warning, and no reason code. | | Code mounts a slot no placement targets | The fallback renders forever, which looks exactly like "the user isn't eligible". | The paywall host pattern shrinks this problem to a single slot id, which is why it is worth adopting on its own. To check the rest, ask the running app: ```ts const diagnosis = sdk.diagnoseSlotInventory(); console.log(diagnosis.authoredButUnmounted); // placements that can never show console.log(diagnosis.mountedButUnauthored); // slots that render fallback forever ``` `diagnoseSlotInventory()` diffs the Playbook's placement triggers against the slots this app actually mounted, in both directions. Only the running app knows what it mounted, so this check cannot live in config tooling. Check `configAvailable` before believing an empty result. If no Playbook reached the SDK, `authored` is empty for a completely different reason than "nothing is authored", and the two are indistinguishable without it. ## Why one slot, restated - **One id to get right.** The failure above becomes a single line to check. - **One place to mount.** "Is it mounted?" is answered by reading the layout. - **Routing lives in your app, targeting lives in the Playbook.** The store decides *when*; the Playbook decides *what* and *to whom*. Neither needs to know about the other. - **Server and client agree.** A `402` and a failed `sdk.can()` open the same paywall through the same path, so there is one rendering to style, test, and measure. # Placement Cookbook > Copy-paste patterns for each core surface type and lifecycle callback. This guide provides ready-to-use patterns for each surface type supported by the SDK. ## Surface Types at a Glance | Surface Type | Component | Use Case | |---|---|---| | `banner` | `BannerSlot` | Full-width sticky banner (top/bottom) | | `modal` | `ModalSlot` | Centered overlay dialog | | `in_page` | `InlineEmbedSlot` | Inline card in page flow | | `toast` | `ToastSlot` | Ephemeral auto-dismiss notification | | `button` | `ButtonSlot` | Single CTA button | | `full_page` | `FullPageSlot` | Dedicated full-page (plans/upgrade) | | `cli` | `CliSlot` | CLI-style monospace message | | `in_page` (quota) | `QuotaMeterSlot` | Usage meter (bar/gauge/numeric) | | `in_page` (credits) | `CreditBalanceSlot` | Depleting credit balance display | ## Banner ```tsx ``` > [Try it live → Usage Warning Banner](/revturbine-sdk/playground/#msg-banner) ## Modal ```tsx ``` > [Try it live → Data Export Gate](/revturbine-sdk/guides/entitlements/#gate-modal) :::caution[Modal safety rule] Only request a modal at **safe moments**: after a user action (clicked a button, hit a feature gate), completed a task, or reached a natural transition. Never on passive page render. ::: ## In-Page ```tsx ``` ## Toast ```tsx ``` ## Button ```tsx ``` > [Try it live → Upgrade Button](/revturbine-sdk/guides/placements/#fixed-button) ## Full-Page ```tsx ``` ## CLI ```tsx ``` ## Quota Meter (In-Page) ```tsx ``` > [Try it live → Quota Meter](/revturbine-sdk/playground/#fixed-usage-counter) ## Credit Balance (In-Page) ```tsx ``` ## Lifecycle Callbacks ```tsx import type { PlacementUiPath } from '@revturbine/sdk'; const createPlacementCallbacks = ( sdk: import('@revturbine/sdk').RevTurbineCustomerSdk, placementId: string, ) => ({ onImpression: () => { void sdk.trackTreatmentInteraction({ userId: 'user_123', placementId, interactionType: 'impression', }); }, onDismiss: () => { void sdk.trackTreatmentInteraction({ userId: 'user_123', placementId, interactionType: 'dismiss', }); }, onCtaClick: () => { void sdk.trackTreatmentInteraction({ userId: 'user_123', placementId, interactionType: 'cta_clicked', }); }, }); ``` ## Object Signature Placement Requests Use typed helper creators for placement requests: ```ts import { createSlotPlacementRequest, createEntitlementPlacementRequest, createChainedPlacementRequest, } from '@revturbine/sdk'; const slotRequest = createSlotPlacementRequest('dashboard_banner', 'banner'); const entitlementRequest = createEntitlementPlacementRequest('mp4_download', { componentType: 'modal', }); const chainedRequest = createChainedPlacementRequest('upgrade_follow_up', { slotId: 'settings_footer', componentType: 'in_page', }); ``` ## Route surface Every slot lifecycle event (`slot_evaluated`, `slot_filled`, `slot_suppressed`, `slot_empty`) is stamped with the app **route** it was evaluated on (`slotContextBase.route`, BL-0207 / ruling D-30). Ingestion-driven surface-slot discovery persists that route on the discovered slot, so the same slot rendered at `/projects/42` and `/projects/43` is recognized as one recurring surface instead of two — that's what lets discovery, the placement dashboard, and needs-attention surfacing group slots by *where* they live in your app rather than by every distinct URL a user happened to visit. A route is always a **path**, never a URL — no origin, no query string, no fragment. Query strings are where tokens, emails and search terms live, so they never reach the wire. ### `` boundary `` declares the route pattern for every slot rendered beneath it. It's renderless (no DOM node), and the innermost boundary wins if you nest them: ```tsx import { RevTurbineRoute } from '@revturbine/sdk'; function ProjectLayout({ projectId, children }: { projectId: string; children: React.ReactNode }) { return ( {children} ); } ``` Mounting it is **optional**. Without it, slots fall back to `window.location.pathname` (see below). Mount it once your router knows the route *pattern*, so `/projects/42` and `/projects/43` collapse to one route instead of being reported as two. ### Next.js recipe Pair it with `routePatternFromParams(pathname, params)`, which rebuilds the framework route pattern from `usePathname()` + `useParams()` without the SDK importing `next/navigation`: ```tsx 'use client'; import { usePathname, useParams } from 'next/navigation'; import { RevTurbineRoute, routePatternFromParams } from '@revturbine/sdk'; export function RouteBoundary({ children }: { children: React.ReactNode }) { const route = routePatternFromParams(usePathname(), useParams()); return {children}; } ``` `/projects/p_42/settings` with `useParams()` returning `{ projectId: 'p_42' }` becomes `/projects/[projectId]/settings`; a catch-all param (`{ slug: ['a', 'b'] }`) becomes `[...slug]`. Mount `` once, near the root layout — every slot beneath it inherits the pattern. ### The pathname fallback and `:id` normalization When no `` is mounted (or none of its ancestors declared a route), a slot reports the browser's `window.location.pathname`, with identifier-like segments templated to `:id` so neither PII nor per-entity cardinality reaches discovery. A segment is templated when it looks like a number, a UUID, a long hex string, a prefixed id (`cus_Q1w2E3r4`, `tn_4bcd07d7-…`), an opaque token, or contains an `@` (email-shaped): ```ts import { normalizeSlotRoute } from '@revturbine/sdk'; normalizeSlotRoute('/accounts/4821/billing?tab=x'); // → '/accounts/:id/billing' (numeric id templated, query string dropped) normalizeSlotRoute('/projects/9b1f2e3a-4c5d-4e6f-8a9b-0c1d2e3f4a5b'); // → '/projects/:id' (UUID templated) ``` A route pattern segment the host already templated (`[id]`, `[...slug]`, `:id`, `*`) is left verbatim rather than re-templated. ### The 512-character cap A normalized route is truncated to **512 characters** if it exceeds that length (`SLOT_ROUTE_MAX_LENGTH`, matching scaffold's `SURFACE_SLOT_ROUTE_MAX_LENGTH`) before it's stamped on an event — an unbounded pathname (deep-linked search state, long catch-all segments) never grows the event payload without limit. ### The headless route option Outside React, pass `route` directly to a headless `PlacementController` — a string, or a getter read at emission time so a long-lived controller reports the route it was evaluated on after a client-side navigation: ```ts import { initRevTurbine } from '@revturbine/sdk/headless'; const session = await initRevTurbine({ tenantId: 'tenant_abc', publicKey: 'rtk_…', endpoint: 'https://edge.example.com', mode: 'snippet', }); const ctrl = session.placement({ surfaceSlot: { id: 'pricing_banner' }, route: () => window.location.pathname, }); ``` Both forms are normalized the same way as the React path. If `route` is omitted, a headless controller emits `route: null` — there's no browser `window.location` fallback outside React, so headless callers that want route attribution supply it explicitly. # Placements > Slot-based and entitlement-based placement patterns — how the SDK decides what to show, where, and when. **Tip — Visual gallery:** Each built-in slot component has a co-located Storybook story with 3-5 variants. Run `pnpm storybook` in the SDK repo to browse them, or see the [Playground](/revturbine-sdk/playground/) for live Sandpack demos. 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 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 ## Slot Configuration Every slot needs an `id` and optionally a list of accepted `surfaceTemplateIds`: ```tsx ``` | Field | Type | Description | |---|---|---| | `id` | `string` | Unique slot identifier (matches server-side placement rules) | | `surfaceTemplateIds` | `string[]` | Templates this slot accepts (filters available placements) | :::tip[Design for the empty state] A slot renders nothing whenever no placement matches the current user — targeting, frequency caps and cooldowns all produce that outcome. Lay the page out so it holds up with the slot empty, and don't reserve space that only looks right when it's filled. Where that isn't possible, give the slot a `fallback` so there is baseline UI when nothing renders. ::: ## Slot Types ### Fixed Slots Always-visible inline placements — buttons, cards, meters: ```tsx 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 ( ); } ``` Fixed slots render nothing if no placement matches — your layout stays intact. > [Try it live → Upgrade Button](/revturbine-sdk/guides/placements/#fixed-button) ### Access Gate Slots Wrap premium content and show an upgrade prompt when access is denied: ```tsx 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 ( ); } ``` 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. > [Try it live → Data Export Gate](/revturbine-sdk/guides/entitlements/#gate-modal) ### Message Slots Page-level overlays triggered by targeting rules — toasts, modals, banners: ```tsx 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 ( ); } ``` Message slots render nothing until a targeting rule triggers. Place them at the top level of your layout. :::caution[Modals only at safe moments] A modal takes over the screen. Request one only when the user has just performed an action, completed a task, or reached a natural transition — never on passive page render. Banners, inline cards, buttons and toasts are safe on render. ::: > [Try it live → Usage Warning Banner](/revturbine-sdk/playground/#msg-banner) ## Placement Output When a placement resolves, the decision contains: ```ts interface PlacementOutput { output_id: string; category: string; surface: { type: RevTurbineComponentType; template?: string; slot_id?: string; }; content: Record; // toast duration is in seconds cta_path?: Record; promotion?: Record; 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 ### Impression → Interaction → Outcome ```text ┌─────────┐ ┌──────────┐ ┌──────────┐ │ Resolve │ ──► │ Visible │ ──► │ Interact │ └─────────┘ └──────────┘ └──────────┘ │ │ │ ┌─────┴─────┐ │ │ dismiss │ │ │ snooze │ │ │ ctaClick │ │ │ ctaComplete│ │ └───────────┘ │ ▼ (auto-tracked impression) ``` ### Interaction Methods ```tsx 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. ## Cap Policies Placements can author cap policies that limit how often they appear: ```ts 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 A CTA can point to another placement via `cta_path.placement_handle`: ```ts 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; ``` ## 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: ```tsx 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); }, }, }} > ``` 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](/revturbine-sdk/getting-started/server-side/#server-owned-cta-actions). ## Using the Hook For custom rendering, use `usePlacement` directly: ```tsx 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 One scenario runs here; the full gallery — every built-in slot type, editable, switching demo users — lives on the [Interactive Playground](/revturbine-sdk/playground/). **Upgrade Button** — an always-visible CTA in a nav slot. _Live example: /playground/#fixed-button_ More on the Playground: [Plans & Pricing Card](/revturbine-sdk/playground/#fixed-in-page) · [Quota Meter](/revturbine-sdk/playground/#fixed-usage-counter) · [Annual Nudge Banner](/revturbine-sdk/playground/#fixed-banner) · [Usage Warning Banner](/revturbine-sdk/playground/#msg-banner) · [Usage Exhausted Modal](/revturbine-sdk/playground/#msg-modal) · [Trial Welcome Toast](/revturbine-sdk/playground/#msg-toast). ## 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](/revturbine-sdk/guides/placement-cookbook/#route-surface) in the Placement Cookbook for the `` boundary, the Next.js recipe, and the pathname fallback. ## Next Steps - [Component Gallery](/revturbine-sdk/components/) — interactive demos of every built-in slot component - [Custom Slot Types](/revturbine-sdk/guides/custom-slots/) — register your own slot components - [Events & Analytics](/revturbine-sdk/guides/events/) — track impressions and interactions - [Interactive Playground](/revturbine-sdk/playground/) — try all slot types live # Plans Without a Subscription > Supply an explicit free-plan identity and understand application versus hosted plan context. A free user does not need a Stripe subscription. They do need an explicit plan identity when a Playbook rule targets a plan. The SDK resolves `plan_handle` first, then `plan.handle`. If neither exists, it does not select the cheapest public plan or invent a default free plan; plan-targeted checks can deny with `no_plan_identity`. ## Choose your no-subscription policy Define a free plan in the Playbook and have your application supply its handle for users who should receive it. Public visibility is a merchandising setting, not an instruction to assign that plan to every unidentified user. Keep the same policy on your backend's authoritative context builder. ```ts rt.update({ plan_handle: 'free' }); ``` Use your actual Playbook handle. Do not translate a display name into a handle or send the retired `plan: { id: ... }` shape. ## Hosted context and application context Hosted client-context delivery can provide a plan derived from the current customer/account state. When a server plan is present, it can replace an earlier application-supplied plan. When the response omits a plan, that omission does not necessarily clear the SDK's existing application plan. Therefore, a missing subscription is not a universal reset-to-free operation. After cancellation, account change, or logout, refresh the authoritative state and explicitly apply or clear the relevant context. Cancellation timing also matters: cancellation at period end may intentionally retain paid access until the paid period ends. Do not infer that all canceled Stripe states have the same access policy. An application-supplied plan can provide an immediate UI update while billing synchronizes. It is not payment proof. The backend must derive access from trusted billing/application records, and a later hosted plan can supersede the optimistic value. Test a new free user, a paid user, cancellation at period end, an expired paid period, and a response that contains no plan. Check the held SDK context as well as the final entitlement result. See [identity lifecycle](/revturbine-sdk/guides/identity-lifecycle/) and [Billing & Stripe](/revturbine-sdk/guides/billing/). # Runtime Modes > Local mode and hosted mode, what each gives you, and where decisions run in both. Two modes matter for most apps: **local** and **hosted**. A third, `custom_endpoints`, routes selected context and telemetry calls through your own proxy while Playbook delivery stays with RevTurbine. ## Hosted mode **Hosted mode is how you change monetization without a deploy.** RevTurbine stores and versions your Playbook; the SDK in your app keeps a fresh local copy and decides from it. What it gives you: - **Change without deploying.** Edit plans, entitlements, targeting and prompts in the RevTurbine dashboard, with your coding agent, or through the CLI. Draft, review, launch. The SDK picks up the new release on its next refresh. - **Versioned Playbooks.** Every launch is an immutable Release with history. Roll back by restoring any earlier one. - **Analytics.** Conversion and usage analytics, so you can see what's working. Growth adds deeper analytics and self-driving growth recommendations. - **Signals your app doesn't hold.** Billing status from Stripe, trial state, per-customer exceptions and end-user behavior follow the user. Caps and cooldowns hold across devices, and an exception applies wherever that user shows up. What doesn't change: the RevTurbine SDK runs inside your app, and on your backend where you verify. It evaluates entitlements and placements locally, as a pure function of your Playbook and the current user context. Hosted mode changes where the Playbook and user state come from, not where decisions run. We add no latency, and our availability never affects your product. In code: `runtimeMode: RuntimeMode.Server` (`revturbine_server`), with the browser's `publicKey` or a trusted backend's `apiKey`. Both load hosted Playbooks and evaluate locally. Backend sessions belong to one user and operation; keep server keys out of browser bundles. See [Initialize the SDK](/revturbine-sdk/getting-started/initialization/) and [Server-Side Integration](/revturbine-sdk/getting-started/server-side/#hosted-delivery-on-a-backend). ## Local mode **Local mode ships the Playbook with your app.** You version it in your own repo, and changing it is a deploy. No account, always free, and production-capable on its own. Every decision is made the same way as in hosted mode. In code: `runtimeMode: RuntimeMode.LocalOnly` (`local_only`) with `localRuntime: { playbook }`. ## Which one 1. Getting started, building a demo, or shipping a Playbook that rarely changes? **Local.** 2. Want to change monetization without deploying, see analytics, or set per-customer exceptions? **Hosted.** 3. Need context or telemetry calls to go through your own proxy? **Hosted, with `custom_endpoints`.** ## Mode comparison | Mode | Where the Playbook comes from | What RevTurbine provides | Required config | |---|---|---|---| | **Local** (`local_only`) | Bundled with your app | Nothing at runtime; optional anonymous adoption beacon | `localRuntime: { playbook }` | | **Hosted** (`revturbine_server`) | RevTurbine, cached locally and refreshed in the background | Playbook delivery and versioning; user state (billing, trials, exceptions, behavior); analytics | `publicKey` (and a `clientSession` route for server-derived state). `tenantId` and `endpoint` are optional | | **Hosted with custom endpoints** (`custom_endpoints`) | RevTurbine, cached locally as in hosted mode | Same as hosted; selected context and telemetry calls use your endpoints | Hosted config + `endpointOverrides` | ### Keep the Playbook intact Use a Playbook checked into your app for local mode, or let the SDK retrieve the launched Playbook from RevTurbine in hosted mode. Pass the validated artifact intact. An app endpoint that reshapes, filters or strips fields creates a different artifact from the one you validated. `endpointOverrides` does not reroute Playbook delivery. Keep `endpoint` set to RevTurbine and override only the supported context and telemetry calls. See the [endpoint reference](/revturbine-sdk/reference/configuration/#revturbineendpointoverrides) for the complete list. ## Where decisions run, and what is cached In every mode the SDK evaluates entitlements and placements inside your app, from the Playbook it holds. No check and no placement decision makes a network call. In `revturbine_server` mode the SDK fetches the launched Playbook at init, keeps it in memory, and re-checks for a new release at most once a minute — a launched Playbook is immutable per version, so polling more often would only waste requests. If a refresh fails, the SDK keeps the last-known-good Playbook and never throws. Before the first Playbook has arrived, an entitlement check denies with an infrastructure reason (`config_unavailable`) rather than guessing. Set the mode with the typed **`RuntimeMode`** constant rather than a raw string: ```ts import { RuntimeMode } from '@revturbine/sdk'; // runtimeMode: RuntimeMode.LocalOnly | RuntimeMode.Server | RuntimeMode.CustomEndpoints ``` `local_only` makes no config or decisioning calls and does not send clickstream events to RevTurbine ingest. A keyless, anonymous `sdk_init` adoption beacon is the one network exception. Set `anonymousTelemetry: false` (or `previewMode: true` for docs and examples) when the process must make zero RevTurbine requests. ## Copy-Paste Setup **Local-only (start here)** ```ts import { initRevTurbine, RuntimeMode } from '@revturbine/sdk'; import playbook from './revturbine.playbook.json'; // No account, key, tenant or endpoint: the Playbook is the whole configuration. const session = await initRevTurbine({ runtimeMode: RuntimeMode.LocalOnly, localRuntime: { playbook, storageKey: 'my-app:revturbine-local-runtime', }, }); ``` **Tip:** In `local_only` mode the SDK evaluates all targeting rules, entitlements, and placement decisions client-side using the loaded Playbook. Clickstream events are not sent to RevTurbine ingest; local consumers and runtime persistence still work. The one network exception is the anonymous adoption beacon described above. **Hosted mode** ```ts import { initRevTurbine, createServerRuntimeConfig } from '@revturbine/sdk'; const session = await initRevTurbine( createServerRuntimeConfig({ tenantId: 'tenant_abc', publicKey: 'rtk_…', // public key — safe in the browser }), ); ``` **Custom endpoints** ```ts import { initRevTurbine, createCustomEndpointRuntimeConfig } from '@revturbine/sdk'; const session = await initRevTurbine( createCustomEndpointRuntimeConfig({ tenantId: 'tenant_abc', publicKey: 'rtk_…', // public key — safe in the browser endpointOverrides: { clientContext: 'https://proxy.example.com/revturbine/client-context', userContext: 'https://proxy.example.com/revturbine/user-context', trialStatus: 'https://proxy.example.com/revturbine/trial-status', ingestEvents: 'https://proxy.example.com/revturbine/track', touchpointTransition: 'https://proxy.example.com/revturbine/interactions', }, }), ); ``` ### Add the context and UI your Playbook uses Local mode needs no placeholder tenant, API key or endpoint. Add a user when your rules depend on identity or plan, resolve the CTA action types your Playbook authors, and optionally supply branding: ```ts import { initRevTurbine, RuntimeMode } from '@revturbine/sdk'; import playbook from './revturbine.playbook.json'; const session = await initRevTurbine({ runtimeMode: RuntimeMode.LocalOnly, localRuntime: { playbook }, user: { id: 'user_123', plan_handle: 'starter' }, uiPathResolvers: { navigate_to_plans: () => { window.location.assign('/pricing'); }, }, branding: { workspace_name: 'My app', theme: { colors: { primary: '#6366f1' } }, }, }); // The async initializer returns a session; instance-only methods live on .sdk. const branding = session.sdk.getBranding(); ``` Use your actual plan handle and CTA action types. Branding is optional and merges over defaults. With React, pass the same options to [RevTurbineProvider](/revturbine-sdk/getting-started/react/) and let it initialize the SDK. See [User Context](/revturbine-sdk/user-context/), [CTA actions](/revturbine-sdk/guides/placements/#cta-actions) and [Theming](/revturbine-sdk/guides/theming/) for the individual options. ## Migrating from local to hosted mode When you want dashboard-managed changes without deploying your app, switch the connection options. Local mode is also production-capable; your component code stays the same: ```diff ``` ## Provider Fallback Strategy Providers are for teams that supply their own facts — entitlement grants, user context — from more than one source. Decisions evaluate locally from the available Playbook. Loading or refreshing that snapshot can still fail; handle `config_unavailable` and choose an explicit freshness policy. Most apps configure no provider. **Available options:** - `provider`: primary provider. - `providerFallbacks`: ordered fallback provider list. - `providerFailureSlotBehavior`: `'invisible'` (default) or `'placeholder'`. **Behavior:** 1. SDK calls primary provider first. 2. If primary fails, SDK logs a warning and executes fallbacks in order. 3. If all configured providers fail for a method, slots render hidden or placeholder output based on `providerFailureSlotBehavior`. ## Integration Checklist 1. Start with `local_only` to validate your integration and placement slots. 2. For dashboard-managed changes and analytics, switch to `RuntimeMode.Server` and provide real credentials. 3. Re-check billing-critical actions on your server with the same Playbook — see [Server-Side Integration](/revturbine-sdk/getting-started/server-side/). 4. If you supply your own facts through providers, choose `providerFailureSlotBehavior` per surface. # Upgrade an SDK Integration > Check identity, entitlement mirroring, gate behavior, and presentation contracts when upgrading to SDK 0.12.4. Pin the SDK version in your package manifest and lockfile. This guide describes the published TypeScript SDK **0.12.4**; review the [changelog](https://github.com/revt-eng/revturbine-sdk/blob/main/CHANGELOG.md) for your starting version. TypeScript, Python, and Rust do not expose identical integration APIs even when their evaluator results agree. ## Entitlement results and mirrors An unknown handle denies with `entitlement_not_in_playbook`. Check spelling and the loaded Playbook before presenting that operational error as an upsell. Read `allowed`, not `status`, because a `limited` outcome can permit or deny work. `UserContext.entitlements` contains application-supplied mirrored grants. These can determine a result before ordinary Playbook rules; `entitlement_mirrored` identifies that path. Do not populate every handle with placeholder booleans and then expect the rules to decide. Supply mirrors only when the application owns those grants and select `entitlementMerge` deliberately. Test a conflicting mirror and rule, an absent mirror, and both a free and paid user. Parameter kinds such as `price_per_unit` and `rate_limit` are not access gates. They deny `can()` with `not_access_entitlement_kind`; see [parameter entitlements](/revturbine-sdk/guides/entitlements/#parameter-entitlements). ## Gates, slots, and presentation A denied gate resolves Access Gate content. A generic promotional placement is not interchangeable with a denied gate's upgrade surface. Keep a safe `deniedFallback` for cases with no eligible Access Gate content. Presentation caps may come from payloads and placement settings. Check the authored category, eligibility, and presentation settings when content does not appear. Message slots do not consume fixed-content placements. Test the actual mounted component with its authored slot and template, not just a raw placement lookup. The public components are `Gate` and `Slot`; this version does not export `InlineSlot`, `OverlaySlot`, or a `moment()` API. Mounted gates recheck when the SDK publishes relevant context or usage updates. You do not need a React key change to make a new SDK balance take effect. Changes in another device or a billing webhook still need to reach the local SDK first. See [React updates](/revturbine-sdk/getting-started/react/#keeping-mounted-gates-current). ## Events and lifecycle Do not emit the removed `growth_signal_observed` event. Use the current typed event/trigger surface for supported signals, and `track()` for your application events. [Billing event ownership](/revturbine-sdk/guides/events/#billing-facts-and-application-events) explains how Stripe facts differ from app telemetry. On a backend, create an SDK session for one verified user and operation and dispose it in `finally`. Sharing immutable Playbooks is safe; sharing a mutable user-bound session across callers is not. Browser sign-in, sign-out, and account switches should follow [identity lifecycle](/revturbine-sdk/guides/identity-lifecycle/). ## Verify the migration Run your application's typecheck and focused integration tests against the exact package you install. Exercise a paid grant, a free denial, a missing Playbook, a misspelled handle, a usage threshold crossed while a gate stays mounted, and user A switching to user B. Recheck the backend operation as well as the UI. The [Gate](/revturbine-sdk/tutorials/gate-premium-feature/) and [usage meter](/revturbine-sdk/tutorials/usage-quota-meter/) tutorials use the current API shapes. The [cloud examples](/revturbine-sdk/guides/cloud-entitlements/) exercise real SDK behavior with local provider adapters; no cloud deployment is required to run them. # Theming > Customize the appearance of built-in slot components with themes. Built-in slot components (banner, modal, toast, etc.) use a theme system for colors, typography, shapes, and shadows. You can use the default theme, apply a built-in variant, or define your own. ## Theme Structure ```ts interface RevTurbineTheme { colors: RevTurbineThemeColors; typography: RevTurbineThemeTypography; shape: RevTurbineThemeShape; shadows: RevTurbineThemeShadows; } ``` ## Color Tokens | Token | Default | Purpose | |---|---|---| | `primary` | `#1e40af` | Primary CTA buttons, links | | `primaryText` | `#ffffff` | Text on primary background | | `secondary` | `#f3f4f6` | Secondary buttons, backgrounds | | `secondaryText` | `#1f2937` | Text on secondary background | | `accent` | `#7c3aed` | Highlight elements | | `accentText` | `#ffffff` | Text on accent background | | `background` | `#ffffff` | Page background | | `surface` | `#f8fafc` | Card/surface background | | `surfaceBorder` | `#e2e8f0` | Card borders | | `text` | `#111827` | Primary text | | `textSecondary` | `#4b5563` | Secondary/description text | | `textMuted` | `#6b7280` | Muted/hint text | | `overlay` | `rgba(0,0,0,0.5)` | Modal backdrop | | `success` | `#16a34a` | Success states | | `warning` | `#f59e0b` | Warning states (quota 70–90%) | | `danger` | `#dc2626` | Danger states (quota > 90%) | | `info` | `#60a5fa` | Informational states | | `track` | `#e5e7eb` | Meter/progress track background | | `toastBackground` | `#1f2937` | Toast notification background | | `toastText` | `#ffffff` | Toast notification text | ## Typography Tokens | Token | Default | Purpose | |---|---|---| | `fontFamily` | `system-ui, -apple-system, sans-serif` | Body text | | `fontFamilyMono` | `ui-monospace, SFMono-Regular, ...` | Code/CLI text | | `fontSize` | `14px` | Base font size | | `fontSizeSmall` | `13px` | Small text | | `fontSizeHeader` | `20px` | Section headers | | `fontSizeLargeHeader` | `28px` | Page/modal titles | ## Shape Tokens | Token | Default | Purpose | |---|---|---| | `borderRadiusSmall` | `6px` | Buttons, inputs | | `borderRadius` | `8px` | Cards, panels | | `borderRadiusLarge` | `12px` | Modals, large surfaces | ## Shadow Tokens | Token | Default | Purpose | |---|---|---| | `medium` | `0 10px 40px rgba(0,0,0,0.25)` | Toast, dropdown | | `large` | `0 20px 60px rgba(0,0,0,0.3)` | Modal | ## Applying a Custom Theme Pass an optional `branding` value in your SDK options. This local React example changes a few tokens and lets the rest use defaults: ```tsx import { RevTurbineProvider } from '@revturbine/sdk'; import { useMemo, type ReactNode } from 'react'; import playbook from './revturbine.playbook.json'; const customTheme = { colors: { primary: '#6366f1', // Indigo primaryText: '#ffffff', accent: '#ec4899', // Pink accentText: '#ffffff', surface: '#fafafa', surfaceBorder: '#e5e5e5', text: '#171717', textSecondary: '#525252', // ... other tokens use defaults }, typography: { fontFamily: '"Inter", system-ui, sans-serif', fontSizeHeader: '18px', }, shape: { borderRadius: '12px', borderRadiusSmall: '8px', }, shadows: { large: '0 25px 50px rgba(0,0,0,0.15)', }, }; export function App({ children }: { children: ReactNode }) { const options = useMemo(() => ({ localRuntime: { playbook }, branding: { theme: customTheme }, }), []); return ( {children} ); } ``` Partial themes are merged with defaults — you only need to specify the tokens you want to change. ### Light and dark palettes Set the provider's `colorScheme` prop to `'light'`, `'dark'` or `'system'` (the default). System follows the device's preference. Pass your app's current preference to that prop when the user changes it; placements repaint without reinitializing the SDK. Keep it outside `options`. `useRevTurbine().colorScheme` reports the resolved `'light'` or `'dark'` value. The selected palette supplies the defaults; explicit branding tokens still override those defaults. ## Accessing the Theme ### In React Components ```tsx import { useRevTurbineTheme } from '@revturbine/sdk'; function ThemedBadge({ label }) { const theme = useRevTurbineTheme(); return ( {label} ); } ``` ### In Custom Slot Components Custom slot components receive the theme as a prop: ```tsx function MySlot({ content, theme }: PlacementSlotProps) { return (
{content?.body}
); } ``` ## Theme Priority The theme is resolved from multiple sources, in priority order: 1. **Explicit `branding` option** — supplied by your app at initialization 2. **Branding API** — supplied or fetched workspace branding 3. **Legacy config `theme`** — accepted for older artifacts, deprecated for new integrations 4. **Defaults** — the selected light or dark palette The selected source's partial theme merges over defaults. For local mode, pass branding explicitly or use defaults. A legacy config theme still resolves for older artifacts; new Playbooks do not need one. An app-mounted `RevTurbineThemeProvider` above `RevTurbineProvider` owns the rendered theme and takes precedence over this automatic resolution. Development builds warn about that override. Use this wrapper when your app deliberately manages the rendered theme; otherwise let `RevTurbineProvider` resolve it. ## Next Steps - [Custom Slot Types](/revturbine-sdk/guides/custom-slots/) — build components that use the theme - [Component Gallery](/revturbine-sdk/components/) — built-in slot demos with theme switching # Troubleshooting > Common SDK integration failures — symptoms, causes, and fixes. ## Troubleshooting Matrix | Symptom | Likely Cause | Fix | |---|---|---| | `getPlacement` returns `null` unexpectedly | Slot/component mismatch or no eligible payload | Verify `slotId`, `componentType`, and payload targeting. Start with `createSlotPlacementRequest(...)`. | | A slot renders nothing, with no error and no reason code | The placement targets a slot your code never mounts — it can **never** show | Run [`sdk.diagnoseSlotInventory()`](#nothing-renders-and-nothing-explains-why). Comparing slot ids by eye is what this replaces. | | A slot renders its fallback forever | Your code mounts a slot no placement targets | Same probe — it reports this direction too. | | Entitlement check denies with `config_unavailable` or `sdk_disabled_provider_failure` | Playbook fetch or configured provider failed | Verify endpoint availability, auth headers, and provider health. See [Client vs Server Enforcement](/revturbine-sdk/concepts/enforcement/) for the authoritative fallback contract. | | CTA path not firing expected action | Payload action field mismatch (`cta_path` vs legacy shape) | Use canonical `cta_path` in payloads and parse via `PlacementRenderer`. | | Decisions feel stale | Cache TTL too long | Lower `ttlMs` in decision requests or call refresh flows explicitly. | | Interactions not visible in backend telemetry | Ingestion endpoint misconfigured | Validate `ingestEvents`/`touchpointTransition` endpoint wiring and auth. | | Runtime mode behavior is incorrect | Wrong mode or incomplete mode config | Re-check mode selection in the [runtime modes guide](/revturbine-sdk/guides/runtime-modes/) and use mode helper builders. | | Console warns primary provider failed | The provider chain is failing: entitlement checks deny with `sdk_disabled_provider_failure` and slots follow `providerFailureSlotBehavior` | Configure `providerFallbacks`, verify provider health, and choose `providerFailureSlotBehavior` intentionally. | | Type is `unknown` from SDK route | Contract/schema title mismatch | Ensure the SDK version matches the schema version and reinstall. | | Build fails after SDK API changes | Missing migration updates in caller code | Migrate to object-style request helpers and rerun typecheck. | ## Warnings that need an integration fix | Warning or reason | What to inspect | | --- | --- | | Blank user ID | Wait for authentication or intentionally omit `user` for a visitor; never pass an empty ID as a loading placeholder. | | Email-shaped identity | Use a stable internal user/account ID; keep email in its dedicated context field. | | Dropped context keys | Use the flat canonical UserContext shape; free-form targeting belongs under `custom`, not an extra `context` wrapper. | | Missing UI-path resolver | Register the authored action type or remove the unused action. A tracked click alone does not navigate or open checkout. | | `entitlement_not_in_playbook` | Compare the exact handle with the loaded Playbook and confirm the correct tenant/version. | | `not_access_entitlement_kind` | `price_per_unit` and `rate_limit` are parameter kinds; gate on an access entitlement. | | `config_unavailable` | Diagnose delivery/authentication/version admission; do not turn an unavailable policy into a paid grant. | | `VAL-PLN-07` | A numeric cap has no explicit enforcement choice. The default blocks at the cap. Choose `hard_block`, `block_with_upsell`, `degrade`, or `allow_overage` intentionally in the authored rule. | | Playbook `tenant_id` differs from the SDK initialization target (for example, `local`) | Check the imported artifact and the explicit `tenantId` option. For local input, the init target wins and a mismatch warns; align them with the intended tenant rather than suppressing the warning. This is not an instruction to rewrite a signed hosted artifact. | | The server's schema is newer than this CLI's bundled snapshot | Offline validation may lack newer rules. Update the repository's pinned CLI (`npm install -D @revturbine/cli@latest`, or your package manager's equivalent). The warning clears when that CLI's bundled schema version is at least the server stamp; reinstalling the same version does not resolve the lag. | Read `initStatus` inside the actual provider and inspect `getPlaybookLoadFailure()` on an initialized SDK when delivery fails. A once-per-session warning may not repeat on every render; fix the cause and rerun the relevant transition. Do not disable validation globally to hide a warning. See [identity lifecycle](/revturbine-sdk/guides/identity-lifecycle/) and [SDK upgrades](/revturbine-sdk/guides/sdk-upgrades/). ## Nothing renders, and nothing explains why This is the hardest symptom to debug, because **there is no error**. Almost nothing in the SDK throws — a mismatch degrades instead of erroring — so a placement that can never show looks exactly like a user who is not eligible. The usual cause is that a placement targets a slot id your code does not mount, or your code mounts a slot id no placement targets. Neither is visible from the Playbook alone: **a config-side audit cannot see your call sites.** Only the running app knows what it mounted, so ask it: ```ts const diagnosis = sdk.diagnoseSlotInventory(); if (!diagnosis.configAvailable) { // No Playbook reached the SDK. `authored` is empty for a completely // different reason than "nothing is authored" — check this FIRST, because // the two look identical in the lists below. console.warn('No Playbook available; fix initialization before reading the rest.'); } // Placements that can never show: they target a slot nothing renders. console.log(diagnosis.authoredButUnmounted); // Slots that render their fallback forever: nothing targets them. console.log(diagnosis.mountedButUnauthored); ``` Each finding names the placement id and category, so it points at the thing to fix rather than telling you something is wrong somewhere. If both lists are empty, `configAvailable` is `true`, targeting matches the user, and the placement still does not render — stop. That is a correct configuration producing silence, which is not a configuration problem. [Open an issue](https://github.com/revt-eng/revturbine-sdk/issues/new) with the diagnostics output above and the SDK version, rather than working around it. Requires `@revturbine/sdk` 0.8.0 or newer. ## Provider Failure Behavior When all configured providers fail for `getPlacement`, `checkEntitlement`, or `identify`, the SDK disables itself to avoid inconsistent behavior. In this state: 1. The SDK logs warning messages to the console. 2. Placements render as hidden (`invisible`) or safe placeholders (`placeholder`) depending on `providerFailureSlotBehavior`. 3. Entitlement checks return a denied result with `sdk_disabled_provider_failure`; see [Client vs Server Enforcement](/revturbine-sdk/concepts/enforcement/) for why checks and placements have different fallback behavior. ## Quick Checks 1. **Run API type checks:** ```bash pnpm typecheck:sdk-api ``` 2. **Run web build:** ```bash pnpm build ``` 3. **Validate SDK docs generation:** ```bash pnpm docs:sdk ``` # Usage Periods and Reset Ownership > Keep authoritative counters, period boundaries, and SDK snapshots consistent. Your application owns usage counting, storage, concurrency, and resets. The SDK evaluates the balance supplied in UserContext against the Playbook's policy. An authored `period_scope` describes the intended period; it does not schedule a database reset or roll a counter over automatically. ## Define the period in your application The authored `period_scope` values are `per_second`, `per_minute`, `per_hour`, `per_6_hours`, `per_day`, `per_week`, `per_month`, `per_year`, `per_billing_period`, `lifetime`, `concurrent`, and `per_instance`. Choose the value that matches your application-owned accounting model. `concurrent` and `per_instance` describe a population or instance boundary, not a scheduled time reset; `lifetime` does not imply periodic replenishment. Decide whether a monthly allowance means a calendar month or a customer's billing-cycle month, which timezone defines the boundary, and what happens on upgrade, downgrade, cancellation, and retry. Store that period identity with the counter so delayed requests cannot consume or reset the wrong period. At a boundary, atomically open the new period in your backend and deliver its current balance to the SDK. A `reset_date` can describe when a reset is due; changing that date alone does not reset the reported amount. ## Report absolute values Initialize usage with the rich `UserUsageEntry` shape. For later changes, use `update({ usage: { api_calls: total } })` with the latest **absolute** total, not the delta for the last request. Treat delayed or out-of-order refreshes according to your application's versioning policy. ```ts // After the backend has accepted the operation and returned the current total: rt.update({ usage: { api_calls: 951 } }); ``` A missing balance can be interpreted as zero used. That is not evidence of an unused account: ensure every metered handle has authoritative usage before allowing a valuable operation. UI updates keep mounted gates current, but the backend remains the enforcement boundary. ## Enforce concurrent operations Two requests can both observe the same remaining allowance. A local SDK check does not reserve it. Use an atomic database update, reservation, or your own metering service to prevent overspending, and then report the resulting balance. Telemetry delivery is not a durable accounting write. Test the last permitted operation, the first denied operation, two concurrent requests for the last unit, and a request across the reset boundary. The [quota-meter tutorial](/revturbine-sdk/tutorials/usage-quota-meter/) covers the UI, while [server-side integration](/revturbine-sdk/getting-started/server-side/) covers per-operation SDK sessions. # Debugging Decisions > Use explainPlacementDecision() to see exactly why a placement was or wasn't shown — segment predicate pass/fail, entitlement-rule matching, and decision metadata. "Why isn't my upgrade prompt showing?" is the most common integration question, and RevTurbine answers it directly. `explainPlacementDecision()` returns the same decision the engine made, plus the full reasoning behind it. ## explainPlacementDecision() Call it with the same input you pass to `getPlacementDecision()`: ```ts // A headless session exposes the SDK as `session.sdk`; in React it is `useRevTurbine().sdk`. const explanation = await session.sdk.explainPlacementDecision(decisionInput); ``` The result is a structured diagnostic covering three things: - **Segment membership** — each configured segment, whether the user matched, and **predicate-level pass/fail** (which trait comparison succeeded or failed). This is where a mistyped trait or a missing user-context field shows up. - **Entitlement-rule matching** — which entitlement rules applied to the current targeting context and their outcomes. - **Decision metadata** — the `rule_id` that won, reason codes, and any **suppression** (e.g. a frequency cap or cooldown that hid an otherwise-eligible placement). ## Reading it When a placement doesn't show, walk the explanation top-down: 1. **Did the user match the segment?** If a predicate failed, check the trait you set in user context against the segment's `field` / `operator` / `value`. 2. **Did an entitlement rule apply?** A gate that grants access won't render an upsell — denial is what triggers the prompt. 3. **Was it suppressed?** Reason codes will name a cap, cooldown, or priority loss to a competing placement. Because the explanation is plain data, wire it into your own QA tooling or a customer-facing "why am I seeing this?" panel — it's the same payload the engine used, so it never drifts from the real decision. ## Related - [Placements](/revturbine-sdk/guides/placements/) — the decision input and how placements resolve - [Run an Experiment with Segments](/revturbine-sdk/guides/experiments/) — segment predicates in practice - [Production Readiness](/revturbine-sdk/operate/production-readiness/) — the rest of the pre-launch checklist # Production Readiness > The pre-launch checklist for a billing-adjacent SDK — keys, versioning, what a check returns when it can't justify a grant, local-only limits, caching, and testing plan changes before you deploy. RevTurbine is safe to run in production from day one: we add no latency and our availability never affects your product. Decisions never have critical dependence on our servers. The RevTurbine SDK runs inside your app, and on your backend where you verify. It evaluates entitlements and placements locally, as a pure function of your Playbook and the current user context. We push additional inputs to your local engine: aggregated frequency caps, prioritization scores as available. These are additive; nothing breaks or blocks if not present. This page is the checklist for the rest. ## Client vs server The one decision that matters most: **the client check drives the UI; your server makes the money-safe decision.** Read [Client vs Server Enforcement](/revturbine-sdk/concepts/enforcement/) before shipping anything billing-critical. ## Keys RevTurbine issues three kinds of key. Two you create in the dashboard; the third your backend mints at runtime. There is no live/test split on keys; use `environmentId` (below) and `test: true` to separate traffic. | Key | Looks like | Lives in | What it does | |---|---|---|---| | **Ingest key** | `rtk_…` (type `public`) | Your browser bundle — safe to ship | The browser's key: Playbook delivery, event ingest, client-safe reads. Bound to an origin allowlist that fails closed: a request from an origin not on the list is refused, as is one with no browser origin at all. Cannot call management APIs. Create it at **Settings → API Token Management → Ingest Keys**, and list every origin that loads the SDK, your local dev origin included. | | **Server key** | `rtk_…` (type `server`) | Your backend only | Programmatic calls to the RevTurbine API from your server — management APIs — and minting the session keys below with `RevTurbineServer`. The SDK's `initRevTurbine` does not read it. Refused on a browser-like request. Create it at **Settings → API Token Management → Server Token**. | | **Session key** | `rt_client_…` | The browser, in memory, short-lived (~10 minutes) | Lets the browser poll one user's server-derived context — plan, trial, payment state — straight from RevTurbine (`GET /api/sdk/client-context`). Your backend mints it per user with the server key (`POST /api/sdk/client-sessions`); the SDK asks for it through your `clientSession` callback and re-mints on expiry. | Which one goes where: - **In the browser**, pass the ingest key as `publicKey`. The name says what it is: a scanner or an agent that finds `publicKey` in a bundle knows it belongs there. As of `0.11.0` it is the **only** name accepted there — `ingestPublicKey` was removed and `apiKey` is no longer read as a browser credential (BL-0113), so a browser init without `publicKey` fails instead of sending the wrong key. Never a server key: RevTurbine refuses one presented from a browser. - **On your backend**, pass the server key as `apiKey` to `new RevTurbineServer({ … })`, which mints session keys (read it from `REVTURBINE_API_KEY`, as [Client sessions](/revturbine-sdk/guides/client-sessions/) does). It is not an option of `initRevTurbine`: your server's checks run from a Playbook snapshot, see [Server-Side Integration](/revturbine-sdk/getting-started/server-side/). - **For server-derived context in the browser**, hand the SDK a `clientSession` callback that returns a session key from your backend. See [Configuration → Server-derived context](/revturbine-sdk/reference/configuration/#server-derived-context-clientsession). - **Local mode needs no key at all.** Two further credentials exist for tooling, not for SDK wiring: the CLI's own `rtk_…` (type `client`) from `revturbine login`, one per machine; and an `rtk_…` (type `mcp`) for agent tooling, from **Settings → API Token Management → MCP Tokens**. Stamp `environmentId` (`'production'` / `'staging'`) so analytics stays separated by deployment, and `test: true` on instances that generate test traffic. ## Versioning Pin `@revturbine/sdk` and the CLI per project; `revturbine --help` from the project-pinned CLI is authoritative for its command surface. A change to any `@public` signature requires a CHANGELOG entry — CI enforces it — and the Playbook schema version the SDK expects is listed on the [Compatibility Matrix](/revturbine-sdk/reference/compatibility/). Pin the SDK to a version whose schema matches the Playbook you deploy. ## No grant, no access A check that can't justify a grant denies rather than leaking a paid feature — the [reason codes](/revturbine-sdk/guides/entitlements/#fail-closed-semantics) say why. The Playbook is cached locally, so real answers need no round trip, and billing-critical actions are re-checked on your server before value is granted. Tune what a slot renders on provider failure with `providerFailureSlotBehavior` (default: render nothing). See [Error Handling](/revturbine-sdk/guides/error-handling/). ## Local-only limits In `local_only` mode the SDK evaluates entirely from the bundled Playbook — no network. That's great for tests and no-backend demos, but be aware: - Usage is **client-reported and client-only** — not authoritative. Don't meter billing off it. - There are **no real-time config updates** — you ship a Playbook snapshot. - Events go to `localStorage`, not the ingest pipeline. See [Runtime Modes](/revturbine-sdk/guides/runtime-modes/) to choose between local mode (`local_only`), hosted mode (`revturbine_server`), and `custom_endpoints`. ## Caching The SDK caches placement decisions and interaction state per user and persists them, so repeat checks are instant and dismiss/snooze survive reloads. The cache clears on identity change (`identify` / `reset`) and when the config updates. If you see a stale decision after a config change, confirm the user context was re-identified. ## Config versioning Every placement decision carries the `config_version` it was evaluated against, so you can trace a decision back to the exact Playbook that produced it. A Playbook moves from **draft → Release**, so each launched change is reviewed, versioned, and traceable. ## Test plan changes before you deploy Because monetization changes ship as config, you can verify them without touching app code: - **CLI:** validate and diff `./revturbine.playbook.json`, or upload it as a draft and run `preview` plus `evaluate --draft --user ` before launch. The [CLI reference](/revturbine-sdk/reference/cli/) lists the required selectors. - **`local_only`:** load a candidate `revturbine.playbook.json` in a test build and exercise the gates and placements against fixture users. - **[Debugging Decisions](/revturbine-sdk/operate/debugging/):** use `explainPlacementDecision()` to see exactly why a decision resolved the way it did. ## Related - [Client vs Server Enforcement](/revturbine-sdk/concepts/enforcement/) - [What Owns What](/revturbine-sdk/concepts/source-of-truth/) - [Debugging Decisions](/revturbine-sdk/operate/debugging/) - [Error Handling](/revturbine-sdk/guides/error-handling/) # Interactive Playground > Every live, editable SDK example in one place — each runs in local_only mode with a demo revturbine.playbook.json. The code is on the page; open any scenario in Sandpack to edit it. Every scenario below is a live SDK demo running entirely in `local_only` mode — a bundled `revturbine.playbook.json` with plans, entitlements, placements, and targeting, and no decision-time network calls. The code is on the page; press **Open in Sandpack** on any scenario to edit it alongside a live preview, and switch demo users to see the engine respond. ## Fixed Slots Always-visible inline placements. ### F-1: Upgrade Button An always-visible upgrade CTA in a nav slot (`button`). _Live example: /playground/#fixed-button_ ### F-2: Plans & Pricing Card An inline pricing/plans card (`in_page`). _Live example: /playground/#fixed-in-page_ ### F-3: Quota Meter A usage meter with progress bar and counter (`in_page`). _Live example: /playground/#fixed-usage-counter_ ### F-4: Annual Nudge Banner An annual-plan nudge banner (`banner`). _Live example: /playground/#fixed-banner_ ## Access Gates Entitlement gates that render an upsell when access is denied. ### G-1: Data Export Gate A blocking modal gate on a paid feature (`modal`). _Live example: /playground/#gate-modal_ ### G-2: Branding Gate An inline gate on a branding feature (`in_page`). _Live example: /playground/#gate-inline_ ### G-3: Brand Kit Gate A card gate on a brand-kit feature (`in_page`). _Live example: /playground/#gate-card_ ## Global Slots Ephemeral, moment-driven messages. ### M-1: Usage Warning Banner A "running low" usage banner (`banner`). _Live example: /playground/#msg-banner_ ### M-2: Usage Exhausted Modal A limit-reached modal (`modal`). _Live example: /playground/#msg-modal_ ### M-3: Trial Welcome Toast A trial-welcome toast (`toast`). _Live example: /playground/#msg-toast_ ## Headless API The imperative SDK, no slot components. ### H-1: PlacementController Driving a placement imperatively. _Live example: /playground/#headless-placement_ ### H-2: EntitlementGate An entitlement check without React. _Live example: /playground/#headless-gate_ ### H-3: SdkSession One-Shot A one-shot session evaluation. _Live example: /playground/#headless-session_ ## How it works Each scenario runs the SDK in an isolated Sandpack environment: 1. **`revturbine.playbook.json`** provides plans, entitlements, placements, surface templates, and targeting rules. 2. **`RevTurbineProvider`** initializes the SDK in `local_only` mode — no network calls. 3. **Demo users** have different plans, usage, and roles: Alice and Bob are on **Professional**, Carol and Dan on **Starter**, and Eve and Frank on **Enterprise**. 4. **Entitlement rules** bind each entitlement to plans by handle — `data_export`, `branding`, and `brand_kit` are granted on Professional and Enterprise and disabled on Starter. So every Access Gate shows its content for Alice, Bob, Eve, and Frank, and its configured upgrade placement for Carol and Dan. 5. **Targeting rules** evaluate against the user context to decide which placements to show. Switch demo users in a scenario's code to see how the SDK renders different placements for different segments. # CLI Reference > Install and pin @revturbine/cli, move a Playbook from draft to Release, select config versions, and generate typed handles. The public `@revturbine/cli` package installs the `revturbine` command. It validates Playbooks offline, manages drafts and Releases, evaluates decisions, generates typed handles, and exposes hosted analytics and ingest-key workflows. ## Install and pin it RevTurbine requires Node.js **22.13 or newer**. Pin the CLI in each repository that owns a Playbook; its version-stamped schema snapshot then travels with the lockfile into local development, CI, and coding-agent sandboxes. ```bash npm install -D --save-exact @revturbine/cli npx revturbine --version ``` Use `pnpm add -D --save-exact @revturbine/cli` or `yarn add -D --exact @revturbine/cli` for those package managers. A global install is optional for ad-hoc work. The pinned CLI warns when another `revturbine` version is running inside the repository. ## The default Playbook loop The canonical filename is `revturbine.playbook.json`. `launch ` performs the guarded file-to-Release path in one operation: validate, stage, and launch. ```bash revturbine download --live --save ./revturbine.playbook.json # edit ./revturbine.playbook.json revturbine validate ./revturbine.playbook.json revturbine diff ./revturbine.playbook.json --live revturbine launch ./revturbine.playbook.json ``` For a separately reviewable draft, use the two-step form: ```bash revturbine upload ./revturbine.playbook.json revturbine preview revturbine validate --draft revturbine launch --draft ``` `diff --live` uses live as the base. A `+` is created on launch; a `-` is pruned on launch unless `--no-prune` is selected. ## First upload of a starter Playbook A starter file may contain a placeholder or origin tenant. After login, select the intended tenant explicitly for the first upload: ```bash revturbine validate --offline ./revturbine.playbook.json revturbine upload ./revturbine.playbook.json -t YOUR_TENANT_ID revturbine validate --draft -t YOUR_TENANT_ID ``` Review the resulting draft before launching. `upload` stages a draft; it does not launch it. `-t`/`--tenant-id` selects the destination but does not grant permission to a tenant your credential cannot access. Do not confuse an SDK public key with a CLI login credential. ## Evaluate without confusing it with the application `evaluate` fetches the selected Playbook and runs the engine locally; RevTurbine does not host a decision endpoint. Supply a user-context file and one version selector, such as `--draft --user ./user.json --entitlement data_export`. Use `--slot` and, when needed, `--component-type` for placement checks. This is a clean evaluation with no live browser suppression or impression history. It does not render React, execute a CTA, or reproduce a mounted Gate's complete denial-and-placement lifecycle. There is no `--gate-placement` option: verify the entitlement and placement inputs separately, then test the real Gate in your application. Use the installed version's `evaluate --help` for its supported flags. ## Version selectors and positional files Commands never guess which config version you mean. | Command | Accepted config input | |---|---| | `download` | exactly one of `--draft`, `--live`, `--release `; `--save ` writes it | | `validate` | one or more positional files, or `--draft` | | `diff` | exactly two from positional files, `--draft`, `--live`, `--release ` | | `show ` | one positional file or one server selector | | `upload ` | one required positional Playbook file | | `launch [file]` | one positional file or `--draft` | | `generate types [file...]` | one positional file or one server selector | | `preview` | the open draft; it takes no config file | | `evaluate` | `--draft`, `--live`, or `--release `; `--user ` is user context, not a Playbook | Run `revturbine --help` for the complete option set from the version your repository pins. ## Command inventory | Command | Purpose | |---|---| | `init` (`create`) | Detect the app, install the SDK, pin the CLI, add a starter Playbook, and install agent skills | | `signup`, `login`, `logout`, `whoami` | Create an account and manage device-flow credentials | | `schema`, `docs` | Emit the bundled schema or print the canonical docs URL | | `download`, `validate`, `diff` | Retrieve and inspect Playbook versions without launching | | `show ` | Summarize plans, entitlements, segments, placements, or trials | | `upload`, `preview`, `launch`, `discard` | Manage the open draft and produce a Release | | `restore`, `status`, `history` | Restore or inspect Releases and draft state | | `evaluate` | Evaluate entitlement or placement decisions locally against a selected server version | | `generate types` | Generate typed Playbook handle objects and literal unions | | `analytics catalog`, `templates`, `views`, `view` | Inspect the hosted Semantic Catalog and accessible analytics views | | `analytics create`, `preview`, `query` | Save, validate, preview, or run canonical analytics-view documents | | `ingest-keys create`, `list`, `revoke` | Manage public SDK telemetry keys; creation prints the secret once | Read commands support `--json` where documented. Results go to stdout and diagnostics go to stderr, so scripts can consume output safely. ## Generate typed handles Generate after authoring a Playbook and again after a launch changes its handles: ```bash revturbine generate types ./revturbine.playbook.json \ --out src/revturbine-handles.ts ``` The generated module contains five families: - `Entitlements`, grouped by entitlement type, plus the `EntitlementHandle` union used by `can()`, `gate()`, and `checkEntitlement()`; - `Plans` and `PlanHandle`; - `Segments` and `SegmentHandle`; - `SurfaceTemplates` and `SurfaceTemplateId`; - `UiPathActionTypes` and `UiPathActionType`. They are `as const` objects plus literal-union types, deliberately not enums. The values are the runtime strings the SDK accepts, so no mapping layer is needed: ```ts import { Entitlements } from './revturbine-handles'; const handle = Entitlements.feature.data_export; const result = await rt.can(handle); ``` The generated header records the exact regeneration command and source selector. Use `--live`, `--draft`, or `--release ` instead of a file when you want types from a hosted version. ## Related - [Installation](/revturbine-sdk/getting-started/installation/) — SDK and CLI setup - [Entitlements](/revturbine-sdk/guides/entitlements/) — use generated handles in checks - [Runtime modes](/revturbine-sdk/guides/runtime-modes/) — local and hosted SDK behavior # Compatibility Matrix > SDK version compatibility, supported browsers, runtimes, and feature support by mode. ## SDK Versions | SDK Version | Schema Version | API Version | Status | |---|---|---|---| | `0.8.x` | `0.1.x` | `v1` | Current | ## Browser Support | Browser | Minimum Version | Notes | |---|---|---| | Chrome | 90+ | Full support | | Firefox | 90+ | Full support | | Safari | 15+ | Full support | | Edge | 90+ | Full support (Chromium-based) | | iOS Safari | 15+ | Full support | | Chrome Android | 90+ | Full support | ### Required APIs The SDK requires these browser APIs: | API | Used For | Fallback | |---|---|---| | `fetch` | Playbook sync, server-derived context, event delivery (not used in `local_only`) | Required — no fallback | | `localStorage` | Decision cache, interaction state | In-memory fallback | | `sessionStorage` | Session state | In-memory fallback | | `JSON.parse/stringify` | Data serialization | Required — no fallback | | `Promise` | Async operations | Required — no fallback | ## React Requirements | Dependency | Minimum Version | Recommended | |---|---|---| | React | 18.0 | 19.x | | React DOM | 18.0 | 19.x | The SDK uses React hooks (`useState`, `useEffect`, `useContext`, `useRef`). React 17 and below are not supported. ## Node.js Requirements | Package | Runtime | Minimum Version | |---|---|---| | `@revturbine/sdk` (web) | Browser | — | | Server SDK (Node.js) | Node.js | 22.13 | ## Build Tool Compatibility | Tool | Support | Notes | |---|---|---| | Vite | ✅ | Recommended | | Next.js | ✅ | App Router and Pages Router | | webpack | ✅ | Version 5+ | | Parcel | ✅ | Version 2+ | | esbuild | ✅ | Used internally for bundling | ## Feature Support by Mode | Feature | `react` | `snippet` | `iframe` | |---|---|---|---| | `RevTurbineProvider` | ✅ | — | — | | React hooks | ✅ | — | — | | Drop-in slot components | ✅ | — | — | | Headless controllers | ✅ | ✅ | — | | Custom slot types | ✅ | — | — | | Theming | ✅ | ✅ | ✅ | | Event tracking | ✅ | ✅ | ✅ | | localStorage persistence | ✅ | ✅ | ✅ | ## Feature Support by Runtime Mode Decisions are evaluated inside your app in every mode. The modes differ in where the Playbook comes from and where telemetry goes. | | `revturbine_server` | `local_only` | `custom_endpoints` | |---|---|---|---| | Decision evaluation (entitlements, placements, caps) | In your app | In your app | In your app | | Playbook source | RevTurbine control plane, cached in the SDK, re-checked about once a minute | Bundled with your app | Your endpoints | | Server-derived context (plan, trial, payment state) | ✅ via client session | Supplied by your app | Your endpoints | | Usage and events | Sent to RevTurbine ingest | Kept locally | Your endpoints | | Playbook changes | Live, without a deploy | Redeploy | Your endpoints | | Cap enforcement | ✅ Client | ✅ Client | ✅ Client | ## TypeScript | TypeScript Version | Support | |---|---| | 5.0+ | ✅ Full support | | 4.9 | ⚠️ May work, not tested | | 4.8 and below | ❌ Not supported | The SDK ships with `.d.ts` type declarations. All public APIs are fully typed. ## Bundle Size Bundle size depends on the entry point, bundler, and imported components. The SDK is tree-shakeable, so measure the exact pinned release in your production build; this page does not publish estimates that can drift from the package. ## Related - [Installation](/revturbine-sdk/getting-started/installation/) — installation instructions - [Configuration Reference](/revturbine-sdk/reference/configuration/) — full options specification # Configuration Reference > Complete InitOptions specification — all configurable parameters with defaults and mode-specific requirements. Complete reference for `RevTurbineInitOptions` and related configuration types. ## RevTurbineInitOptions ### Required Fields In `revturbine_server` and `custom_endpoints` modes the browser needs `publicKey`; `tenantId` is optional because the key names its tenant. In `local_only` mode neither is needed — `initRevTurbine({ runtimeMode: 'local_only', localRuntime: { playbook } })` is a complete configuration. For copy-paste examples per runtime, see [Initialize the SDK](/revturbine-sdk/getting-started/initialization/). | Field | Type | Description | |---|---|---| | `tenantId` | `string` | Your RevTurbine tenant identifier. Optional whenever `publicKey` is supplied: the key is bound to one tenant and the SDK reads the tenant back from the Playbook it fetches. A `tenantId` that disagrees with the key's tenant is ignored with a one-time warning; the key wins. Required by `RevTurbineServer`, where it must be the server key's own tenant. | | `publicKey` | `string` | In the browser: your public key (`rtk_…`, type `public`, minted under **Settings → API Token Management → Ingest Keys**). Safe to ship in a bundle. Add your app's origins to the key's allowed origins, or Playbook delivery is refused with `403 origin_not_allowed`. See [Production Readiness → Keys](/revturbine-sdk/operate/production-readiness/#keys). | | `apiKey` | `string` | Your server key (`rtk_…`, type `server`), minted under **Settings → API Token Management → Server Token**. Trusted backend `initRevTurbine` in `RuntimeMode.Server` uses it for Playbook delivery and event ingest; `RevTurbineServer` uses it for client sessions and server-only writes. Evaluation remains local. Never ship it to a browser. As of `0.11.0` it is not accepted as a browser credential — pass `publicKey` there; `ingestPublicKey` was removed (BL-0113). | ### Runtime Configuration | Field | Type | Default | Description | |---|---|---|---| | `runtimeMode` | `'revturbine_server' \| 'custom_endpoints' \| 'local_only'` | `'revturbine_server'` | How the SDK loads its Playbook; evaluation remains local | | `endpoint` | `string` | `'https://revturbine.com/app'` | RevTurbine control-plane URL; omit it for the hosted service | | `mode` | `'react' \| 'snippet' \| 'iframe'` | `'snippet'` (`'react'` when `` initializes the SDK) | Integration label carried in telemetry; no decision depends on it | | `endpointOverrides` | `Partial` | — | Route the SDK's non-decision calls through your own endpoints (`custom_endpoints` mode) | | `configProvider` | `RevTurbineConfigProvider` | — | Custom provider for the Playbook; implement `getPlaybook()` | | `localRuntime` | `RevTurbineLocalRuntimeOptions` | — | Local-only mode configuration | ### Provider Configuration | Field | Type | Default | Description | |---|---|---|---| | `provider` | `RevTurbineSdkProvider \| RevTurbineProviderFactory` | — | Primary provider override | | `providerFallbacks` | `Array<...>` | — | Fallback provider chain | | `domainProviders` | `AnyDomainProvider[]` | — | Domain-specific providers | | `providerFailureSlotBehavior` | `'placeholder' \| 'invisible'` | `'invisible'` | Slot behavior after provider failure | ### UI Path Handling | Field | Type | Default | Description | |---|---|---|---| | `uiPathResolvers` | `RevTurbineUiPathResolverMap` | — | Map of CTA action types to resolver functions | ### Branding | Field | Type | Default | Description | |---|---|---|---| | `branding` | `BrandingConfig` | Built-in defaults | Optional workspace name, logo and partial theme. An explicit value takes priority over API branding and legacy config themes. | See [Theming](/revturbine-sdk/guides/theming/) for an example. Set `colorScheme: 'light'` or `'dark'` in initialization, use the React provider's `colorScheme` prop, or call `session.sdk.setColorScheme(scheme)` in a headless integration (SDK 0.12.4). ### User & Page Context | Field | Type | Default | Description | |---|---|---|---| | `user` | `RevTurbineUserContext` | — | Initial user context. `plan_handle` is the plan's `unique_handle` — the value plan-scoped rules match on | | `clientSession` | `() => string \| Promise` | — | Mints a client-session token so the SDK keeps server-derived context fresh on its own. See [Server-derived context](#server-derived-context-clientsession) | | `page` | `RevTurbinePageContext` | — | Page context (URL, title, referrer, tags) | | `contextPolicy` | `RevTurbineContextPolicy` | `{ inferUser: true, inferPage: true, routerAutoTrack: true }` | Auto-inference behavior | ### Server-derived context (`clientSession`) Your app tells RevTurbine who the user is. Some of that state, though, is only knowable on your server — the plan a Stripe webhook just changed, trial status, a payment that failed. `clientSession` is how the SDK gets it without you wiring a refresh loop. Supply a function that returns a client-session token minted by your backend (`POST /api/sdk/client-sessions`, with the server key; `RevTurbineServer` does it for you). The SDK calls it when it first needs a token, again after `identify()`, and again if the control plane reports one expired — then fetches `GET /api/sdk/client-context` and folds the result into its decisions. **A purchase updates what the user can do with no further app code.** ```ts import { initRevTurbine } from '@revturbine/sdk'; export async function startRevTurbine() { return initRevTurbine({ publicKey: process.env.NEXT_PUBLIC_REVTURBINE_PUBLIC_KEY!, // public key — safe in the browser user: { id: 'user_123', plan_handle: 'free' }, clientSession: async () => { const response = await fetch('/api/revturbine-session', { method: 'POST' }); if (!response.ok) throw new Error(`RevTurbine client session failed (${response.status})`); const body: { client_token: string } = await response.json(); return body.client_token; }, }); } ``` The route behind `/api/revturbine-session` is yours: it authenticates your user, mints with the server key and returns only `client_token`. The complete route (Next.js and Express), the failure behavior and the refresh rules are on [Client sessions](/revturbine-sdk/guides/client-sessions/). It is a **function, not a token**, because these tokens are short-lived (~10 minutes) — a value captured at init would go stale mid-session, and refreshing it would become your problem. The token is a transport credential, never user context: held in memory only, never persisted, never logged, never placed in a URL. If your minter throws or the fetch fails, the SDK keeps using the context your app supplied — enrichment is best-effort and never breaks your app. Omit `clientSession` and none of this happens: no token is requested and no client-context call is made. Server-derived context is opt-in. ### Behavioral Flags | Field | Type | Default | Description | |---|---|---|---| | `placementBehavior` | `Partial` | derived from the Playbook | Overrides for flags the SDK derives from what the Playbook authors (see below) | ### Storage | Field | Type | Default | Description | |---|---|---|---| | `persistentStorage` | `RevTurbineStorage` | `localStorage` | Persistent storage override | | `sessionStorage` | `RevTurbineStorage` | `sessionStorage` | Session storage override | --- ## RevTurbineContextPolicy | Field | Type | Default | Description | |---|---|---|---| | `inferUser` | `boolean` | `true` | Auto-detect user info from browser APIs | | `inferPage` | `boolean` | `true` | Auto-capture URL, title, referrer | | `routerAutoTrack` | `boolean` | `true` | Track SPA route changes | --- ## RevTurbineLocalRuntimeOptions | Field | Type | Description | |---|---|---| | `playbook` | `Playbook` | Full Playbook snapshot for local execution | | `placements` | `LocalPlacementDataset` | Optional static placements dataset | | `initialData` | `object` | Static data for local decisions (see below) | | `resolvers` | `object` | Optional resolver callbacks (see below) | | `storageKey` | `string` | Optional localStorage key override | | `getContext` | `() => Promise` | Reactive context callback | ### initialData | Field | Type | |---|---| | `placementDecisionsByPlacementId` | `Record` | | `userContextByUserId` | `Record` | | `trialStatus` | `RevTurbineTrialContext` | Entitlement grants are evaluated from `localRuntime.playbook`. For a custom source, implement the documented `checkEntitlement` resolver; do not seed grants through `initialData`. Placements work the same way: `getPlacement` is a pure function of the user context and `localRuntime.playbook`, evaluated through the same decision path as `getPlacementDecision` and the server SDKs. There is no seeded-output lane: `initialData.placementsByLookupKey` was removed in 0.12.0 (ignored since 0.11.21). Author the placement in the Playbook instead, or implement the `getPlacement` resolver for a custom source. ### resolvers | Field | Signature | |---|---| | `getPlacementDecision` | `(input, placement?, context?) => Promise` | | `getPlacement` | `(config) => Promise` | | `checkEntitlement` | `(handle, context?) => Promise` | | `fetchUserContext` | `(userId) => Promise` | | `getTrialStatus` | `() => Promise` | | `resolvePlaybook` | `() => Promise` | --- ## RevTurbinePlacementBehaviorFlags Each flag is derived from the loaded Playbook; an explicit value in `placementBehavior` overrides the derivation. You rarely need to set them. | Flag | Type | Derived default | Description | |---|---|---|---| | `enableClientCapsEnforcement` | `boolean` | `true` when any placement payload authors a cap, cooldown, or remind-later | Client-side cap enforcement | | `enableAutoGatedPlacement` | `boolean` | `true` when the Playbook has a `gated` placement | Auto-render gated placements | | `enableTrialAutoTriggers` | `boolean` | `true` when the Playbook has a `trials` placement | Auto-derive trial lifecycle triggers | --- ## RevTurbineEndpointOverrides The calls the SDK makes over the network, and the key that reroutes each one in `custom_endpoints` mode. None of them is a decision — entitlements and placements are evaluated inside your app in every mode. A relative value is appended to `endpoint`; an absolute URL replaces it. | Key | What it carries | Default path | |---|---|---| | `clientContext` | Server-derived user context, read with an `rt_client_` session token | `/api/sdk/client-context` | | `userContext` | User context read | `/api/sdk/user-context` | | `trialStatus` | Trial status read | `/api/sdk/trial-status` | | `ingestEvents` | Clickstream events (`track` / `capture`) | `/api/track` | | `touchpointTransition` | Placement interactions — impression, dismiss, CTA | `/api/events/interactions` | | `ingestSdkMeta` | The anonymous `sdk_init` beacon | `/api/sdk/meta` | | `surfaceSlots` | Surface-slot inventory registration | `/api/placements` | | `placementTypes` | Custom placement-type persistence — meaningful only when overridden | `/api/sdk/placement-types` | Not overridable: **Playbook delivery**. The SDK fetches the launched Playbook from `endpoint` directly (`/api/sdk/bootstrap`, then the signed manifest and bundle), so `custom_endpoints` routes context and telemetry through your proxy but not the Playbook itself. Keep `endpoint` pointing at RevTurbine and use absolute override URLs for a proxy on another origin. In local mode, import the Playbook checked into your app. In either mode, preserve the validated artifact: do not insert an app endpoint that reshapes or strips Playbook fields before the SDK reads them. See [Runtime Modes](/revturbine-sdk/guides/runtime-modes/#keep-the-playbook-intact). The type also declares `decide`, `decideContext`, `bootstrapContext`, `getPlacement` and `checkEntitlement`. They are retired: the SDK never reads them, because there is no decision endpoint. They remain on the type so existing configurations still compile. --- ## RevTurbineStorage Interface for custom storage providers: ```ts interface RevTurbineStorage { getItem(key: string): string | null; setItem(key: string, value: string): void; removeItem(key: string): void; } ``` --- ## Mode-Specific Required Fields ### `revturbine_server` (default) ```ts docs-check=false reason="required-field shape sketch, not a value" { tenantId?: string; // optional: the key names its tenant publicKey: string; // ✅ Required (public key, browser). `apiKey` is not read here: a server uses a Playbook snapshot instead endpoint?: string; // defaults to 'https://revturbine.com/app' mode?: string; // defaults to 'snippet' ('react' via the provider) } ``` ### `local_only` ```ts docs-check=false reason="required-field shape sketch, not a value" { runtimeMode: 'local_only'; // ✅ Required localRuntime: { playbook: Playbook; // ✅ Required }; // tenantId, publicKey, endpoint, mode — optional; no account or key is needed } ``` ### `custom_endpoints` ```ts docs-check=false reason="required-field shape sketch, not a value" { tenantId?: string; // optional: the key names its tenant publicKey: string; // ✅ Required (public key, browser) endpoint?: string; // defaults to 'https://revturbine.com/app' mode?: string; // defaults to 'snippet' ('react' via the provider) runtimeMode: 'custom_endpoints'; // ✅ Required endpointOverrides: { // ✅ At least one override required clientContext?: string; ingestEvents?: string; touchpointTransition?: string; }; } ``` ## Related - [Runtime Modes](/revturbine-sdk/guides/runtime-modes/) — mode comparison and migration - [Error Codes Reference](/revturbine-sdk/reference/errors/) — error handling options # Error Codes > Enumerated error and reason codes returned by the SDK, with causes and fixes. This page lists all error and reason codes the SDK may return, organized by category. ## Placement Reason Codes Returned in `decision.reasonCodes[]` to explain why a placement was or wasn't shown. This is the complete set the SDK emits — generated from the public reason contract (`tests/reason-contract.json` in revturbine-sdk-internal), the same file [`reason-contract.test.ts`](https://github.com/revt-eng/revturbine-sdk-internal/blob/main/web-sdk/reason-contract.test.ts) enforces against live SDK fixtures. Do not hand-edit the table below; run `pnpm gen:reason-codes` (from `pages-build/`) after the contract changes. | Code | Meaning | |---|---| | `config_unavailable` | The Playbook could not be fetched, or a fetch is still in flight — the SDK has no config to decide against yet. | | `entitlement_gate_unmet` | The candidate's entitlement-gate trigger did not match the user's current entitlement state. | | `entitlement_not_denied` | The Access Gate placement's entitlement is not denied or limited for this user, so the gate does not fire. | | `gate_outside_access_gate` | An Access Gate placement was requested from a slot that is not an Access Gate slot; gates render only in gate slots. | | `no_eligible_candidate` | No candidate placement survived eligibility filtering for this slot. | | `no_gate_for_entitlement` | The Access Gate slot asks for an entitlement that no gate placement is authored for, so the slot shows its access-denied placeholder. | | `no_resolver_configured` | No placement resolver is configured, so the SDK has nothing to evaluate against. | | `placement_not_found` | No candidate output matched the requested placement id or name. | | `placement_not_registered` | The payload targets a placement handle the app never registered with the SDK. | | `placement_retired` | The candidate's rule is hidden — for example, superseded by a conversion — per impression history. | | `plan_target_mismatch` | The user's plan or billing period doesn't match the candidate's plan targeting. | | `qualifier_trigger_unmet` | The candidate's qualifier trigger condition was not satisfied for the user's plan or category. | | `sdk_disabled_provider_failure` | The SDK disabled itself after its configured providers failed, so it fails closed rather than risk showing something wrong. | | `segment_target_mismatch` | The user doesn't belong to any of the candidate's target segments. | | `suppressed_by_dismiss_cooldown` | The user dismissed this placement and its cooldown window has not elapsed. | | `suppressed_by_payload_cap_day` | The payload's per-day impression cap has been reached. | | `suppressed_by_payload_cap_lifetime` | The payload's lifetime impression cap has been reached. | | `suppressed_by_payload_cap_month` | The payload's per-month impression cap has been reached. | | `suppressed_by_payload_cap_session` | The payload's per-session impression cap has been reached. | | `suppressed_by_payload_cap_week` | The payload's per-week impression cap has been reached. | | `suppressed_by_payload_cooldown` | The payload is still inside its configured cooldown window since it was last shown. | | `suppressed_by_presentation_cap` | Every eligible candidate is over an overall presentation cap rule from Placement Settings. | | `suppressed_by_system_cooldown` | Every eligible candidate is a discretionary nudge inside the session cooldown since the last one shown. | | `suppressed_until_remind_window` | The user chose "remind me later" and that reminder window has not elapsed yet. | | `threshold_trigger_unmet` | The candidate's threshold trigger did not match the user's current entitlement usage. | | `trial_trigger_unmet` | The candidate's trial trigger did not match the user's trial state. | | `unknown_trigger_type` | The candidate uses a trigger type this SDK does not recognize, so it is not shown. | :::note[Client-side cap enforcement] When `enableClientCapsEnforcement` is on, the SDK also ticks `cap.v1` presentation-cap policies locally and can add `cap_exceeded` to `reasonCodes` on a cache-hit re-evaluation once the local budget is consumed. This is a client-side fallback default distinct from the `suppressed_by_payload_cap_*` family above (which come from the evaluated Playbook itself) and from the protected reason contract, so it is called out here rather than generated into the table. ::: ## Entitlement Reason Codes Returned in `result.reason` to explain the entitlement check outcome. Entitlement checks are [**fail-closed**](/revturbine-sdk/guides/entitlements/#fail-closed-semantics): when the SDK cannot produce an affirmative grant it denies and names the cause. This is the complete emitted set — see [Error handling](/revturbine-sdk/guides/error-handling/) for the enforcement-mode suffixes on the two limit codes. | Code | Meaning | Fix | |---|---|---| | `no_matching_entitlement_rule` | No rule grants this entitlement to the user's plan — check **denied** | Add an entitlement rule targeting that plan, or check the user's `plan_handle` is the plan's `unique_handle` | | `feature_not_enabled_for_plan` | A matching `feature` rule has `enabled: false` — check **denied** | Enable the rule for that plan, or upgrade the user | | `usage_limit_reached` | At or over a `usage_limit` rule's limit | Report accurate usage via `updateUsage()`; raise the limit or change `enforcement` | | `credit_balance_exhausted` | At or over a `credits` rule's allowance | Grant more credits or change `enforcement` | | `config_unavailable` | The launched Playbook could not be fetched (Server mode) — check **denied** | Check network connectivity; the reason distinguishes a fetch failure from a real denial | | `entitlement_not_in_playbook` | Local mode with no Playbook and no cached result — check **denied** | Add the entitlement to the Playbook fixture | | `sdk_disabled_provider_failure` | The SDK disabled itself after a provider failure — check **denied** | Check API keys, endpoints, and network | | `granted_by_reverse_trial` | **Allowed** by an active reverse trial rather than by the plan | None — expected during a reverse trial | :::note[Renamed in 0.3.0] `local_runtime_default_allow` → `entitlement_not_in_playbook`, no deprecated alias. The old name stated a verdict the result does not have (it denies). ::: ## Provider Errors | Error | Source | Meaning | |---|---|---| | `provider_chain_exhausted` | All providers failed | Check API keys, endpoints, and network | | `config_fetch_failed` | Playbook could not be loaded | Verify `configProvider` or API endpoint | | `invalid_api_key` | API returned 401 | Check the `publicKey` (browser) or `apiKey` (server) value and key status | | `tenant_not_found` | API returned 404 | Verify `tenantId` value | ## Interaction Errors | Error | Context | Meaning | |---|---|---| | `interaction_tracking_failed` | `trackTreatmentInteraction()` | Event delivery failed — silently dropped | | `event_delivery_failed` | `trackEvent()` | Custom event could not be sent — buffered for retry | | `network_error` | Interaction-flush fetch | The touchpoint-transition request threw (offline, DNS, CORS) — the batch is re-queued and retried | ## Storage Errors | Error | Context | Meaning | |---|---|---| | `storage_unavailable` | localStorage/sessionStorage | Browser storage not accessible — using in-memory fallback | | `storage_quota_exceeded` | `setItem()` failed | Clear old entries or use custom storage provider | ## Debugging Enable verbose logging to see all reason codes and errors: ```ts localStorage.setItem('revturbine:debug', 'true'); ``` Errors and reason codes are also available programmatically: ```tsx const { decision, error } = usePlacement({ placement: { name: 'hero_banner' } }); // Hook-level error (string) console.log(error); // Decision-level reason codes console.log(decision?.reasonCodes); ``` ## Related - [Error Handling Guide](/revturbine-sdk/guides/error-handling/) — patterns and strategies # Playbook Field and Reference Map > The 29 body fields, their roles, and the references connecting policy and presentation. A Playbook is a versioned document, not a flat list of entitlements. Its envelope identifies the artifact, tenant, environment, and schema version; its body holds policy, targeting, and presentation. This map describes the 29 top-level body keys in the schema pinned by SDK 0.12.4 documentation (`0.1.520`). Their key set also matches the scaffold source reviewed on October 23, 2026. ## The main reference paths Arrows point from a record that contains a reference to the record it names. This overview groups related fields; the tables below provide their exact names. Playbook policy and presentation reference map Entitlement rules reference entitlements, plans and add-ons, and segments. Variations reference plans and add-ons and segments. Placements reference payloads and slots. Payloads reference message blocks, UI paths and promotions. Message blocks reference surface templates. Entitlement rulesVariations EntitlementsPlans / add-onsSegments PlacementsPayloads / slots Message blocks / UI pathsand promotionsSurface templates ## Body field inventory “Array” means a collection of records, not a promise that each record is an independent runtime entity. Optional fields may be absent. Compatibility and authoring-only fields are not instructions to build new integrations around them. | Field | Shape | Purpose | | --- | --- | --- | | `plans` | Array | Base plan definitions, identified by `unique_handle`. | | `addons` | Array | Add-on definitions referenced by entitlement targets. | | `entitlements` | Array | Named access or parameter capabilities and their kinds. | | `entitlement_rules` | Array | Rules binding an entitlement to plan/add-on targets and optional segments. | | `segments` | Array | Named targeting predicates over context. | | `content_ui_paths` | Array | Authored CTA action definitions and routing parameters. | | `slot_configs` | Array | Deprecated compatibility field; activation/trigger state belongs to the SDK. | | `content_overrides` | Object | Deprecated compatibility map; author Message Block/Payload content instead. | | `theme` | Object | Legacy branding compatibility data; branding is independently owned. | | `placement_slots` | Array | Authored locations into which matching placements may render. | | `message_blocks` | Array | Reusable content shaped for a surface template. | | `placement_payloads` | Array | Content selections, targeting, actions, and presentation for placements. | | `placements` | Array | Placement category, triggers, targeting, and content associations. | | `content_promotions` | Array | Authored promotions that content and actions can reference. | | `personalization_tokens` | Array | Named substitutions used in content. | | `surface_templates` | Array | Presentation templates referenced by message blocks and slots. | | `free_trial_rules` | Array | Authoring definitions for opt-in/free trial behavior. | | `reverse_trial_rules` | Array | Authoring definitions for reverse trials and their fallback/conversion plans. | | `objectives` | Array | Authoring-only business labels; not runtime decision inputs. | | `plan_variations` | Array | Plan-owned pricing variations, with optional segment eligibility. | | `addon_variations` | Array | Add-on-owned pricing variations, with optional segment eligibility. | | `extension_rules` | Array | Tagged extension payloads; unknown mandatory kinds must be rejected by readers. | | `seat_types` | Array | Seat-type policy definitions for account-scoped access. | | `enforcement_defaults` | Array | Configured enforcement defaults used by applicable policies. | | `placement_settings` | Array | Placement presentation and selection settings. | | `segment_dimensions` | Array | Segment-dimension declarations; field presence does not imply a normalized bundle table. | | `meter_bindings` | Array | Bindings connecting policy to meter definitions. | | `experiments` | Array | Reserved compatibility placeholder; the current schema requires an empty array. | | `signal_catalog` | Object | Reserved compatibility placeholder; the current schema requires an empty strict object. Signal definitions remain on the control plane and are not shipped to the browser. | ## References use the target's declared identity Do not assume a field ending in `_id` is a database UUID. Portable references often use handles. These examples follow the schema's reference graph: | Referencing field | Target identity | Meaning | | --- | --- | --- | | Rule `entitlement_id` | Entitlement `unique_handle` | The capability this rule configures. | | Rule `targets[].id`, kind `plan` | Plan `unique_handle` | Applicable plan. | | Rule `targets[].id`, kind `addon` | Add-on `unique_handle` | Applicable add-on. | | Rule segment references | Segment `handle` | Eligibility restriction. | | Variation `plan_handle` / `addon_handle` | Parent `unique_handle` | Owning plan or add-on. | | Variation `segment_handle` | Segment `handle` | Optional price eligibility. | | Trial plan, fallback, or conversion references | Plan handle | Trial entry/exit plan policy. | | Placement trigger entitlement / slot references | Entitlement handle / slot ID | Which event or location can activate the placement. | | Payload segment-content entries | Segment handle | Conditional content selection. | | Payload content/action references | Message block, UI path, promotion identities | Content and action to resolve. | | Message block template reference | Surface template ID | Rendering shape. | | UI-path promotion / placement references | Referenced promotion / placement identity | Destination or offer for the action. | Variation-level rule target enum members exist for compatibility but are not a recommendation to author new variation-level entitlement targeting. Use the supported plan/add-on targeting model. Removing a segment constraint can widen access; do not silently drop a missing reference. Validate the whole document with the CLI before upload and review the diff before launch. The compiler's 13 normalized bundle tables are a projection of this document, not the complete list of Playbook body fields. A body field's presence does not prove it is lowered into an independent table or read by every language port. See [runtime and Playbook](/revturbine-sdk/concepts/runtime-and-playbook/) for the distinction between authored policy and the per-user facts evaluated with it. # Tutorial: Show a Banner Placement > End-to-end tutorial — render a targeted, dismissible banner (an upsell or announcement) with a CTA, controlled from your config. In this tutorial, you'll render a **banner** at the top of your app — a targeted upsell or announcement with a call-to-action — controlled entirely from your config. The banner shows only for the users your config targets, and renders nothing for everyone else. Takes about 10 minutes. ## What You'll Build A header banner that: - Renders a placement in a fixed slot via `` - Shows only to a targeted audience (here, Pro users), and nothing otherwise - Has a dismissible CTA that opens your checkout flow ## Prerequisites - A React 18+ application with `@revturbine/sdk` installed - A `RevTurbineProvider` set up (see [Quickstart](/revturbine-sdk/)) ## Steps 1. **Add a banner placement to your config** This is a minimal but complete `Playbook` — author the same shape in the [dashboard](https://revturbine.com/app) or with the CLI. The banner is a `fixed` placement bound to a named slot, targeted at the Pro plan. ```json title="revturbine.playbook.json" { "artifact_type": "playbook", "format_version": "1.0.0", "plans": [ { "unique_handle": "pro", "name": "Pro", "tier_position": 1, "sort_order": 1 } ], "entitlements": [], "entitlement_rules": [], "segments": [], "content_ui_paths": [], "surface_templates": [ { "id": "banner_placement", "surface_type": "banner", "fields": [] } ], "placements": [ { "id": "pl_annual_upsell", "name": "Annual billing upsell", "category": "fixed", "trigger": { "type": "surface_render", "slot_id": "app_header_banner" }, "payloads": [ { "id": "pl_annual_upsell_p0", "target": { "plan_ids": ["pro"], "segment_chips": [] }, "surfaces": [ { "template_id": "banner_placement", "fields": { "header": "Save 20% with annual billing", "body": "Switch your Pro plan to annual and get two months free.", "dismissible": "Yes" }, "ctas": [ { "label": "Switch to annual", "path": "open_checkout", "config": { "purchase": "pro_annual" } } ] } ], "caps": {}, "status": "active" } ], "order": 0 } ] } ``` 2. **Drop the slot into your header** `` renders the matching placement for the current user — or nothing. The `id` matches the placement trigger's `slot_id`. ```tsx title="src/components/AppHeader.tsx" import { Slot } from '@revturbine/sdk'; export function AppHeader() { return (
{/* Renders the targeted banner, or nothing */}
); } ``` 3. **Provide the user context that targeting reads** The placement targets `plan_ids: ["pro"]`, so the banner only renders for Pro users. Pass the user's plan in the provider; the banner's CTA dispatches through the UI path resolvers you registered at init. ```tsx title="src/App.tsx" ``` ## How It Works 1. `` asks the decision engine which placement wins for `app_header_banner`. 2. The engine evaluates the payload's `target` against the current user (plan, segments). If nothing matches, the slot renders nothing — your header is unchanged. 3. The slot renders the surface's `fields` (header, body) and CTA; clicking the CTA dispatches through your resolvers (see [Quickstart](/revturbine-sdk/)). 4. Dismissing the banner records the interaction, so any `caps` (cooldowns, max-per-period) apply on the next render. **Tip:** A banner is a **fixed** placement — it renders wherever you place the slot, filtered by targeting. For a banner that fires on a *condition* (credits running low, a failed payment), use a trigger instead — see [Warn Users About Low Credits](/revturbine-sdk/tutorials/low-credits-warning/) and [Recover a Failed Payment](/revturbine-sdk/tutorials/payment-recovery/). ## Try It Live > [Interactive Playground → Banner Slot](/revturbine-sdk/playground/#fixed-banner) ## Next Steps - [Placements Guide](/revturbine-sdk/guides/placements/) — targeting, triggers, and surface types - [Tutorial: Add an Upgrade Button](/revturbine-sdk/tutorials/upgrade-button/) — a persistent CTA button instead of a banner - [Component Gallery](/revturbine-sdk/components/) — every built-in slot component # Tutorial: Gate a Premium Feature > End-to-end tutorial — check an entitlement, block access for free users, and show an upgrade modal. In this tutorial, you'll gate a premium feature (video export) behind an entitlement check and show an upgrade modal when access is denied. Takes about 10 minutes. ## What You'll Build A video export button that: - Checks the `data_export` entitlement before allowing access - Shows an upgrade modal when the user doesn't have the entitlement - Works instantly in local mode, no API calls needed ## Prerequisites - A React 18+ application with `@revturbine/sdk` installed - A `RevTurbineProvider` set up (see [Quickstart](/revturbine-sdk/)) ## Steps 1. **Add entitlement and gate placement to your config** Add to your `revturbine.playbook.json`: ```json title="revturbine.playbook.json" { "artifact_type": "playbook", "format_version": "1.0.0", "plans": [ { "unique_handle": "free", "name": "Free", "tier_position": 0, "sort_order": 0 }, { "unique_handle": "professional", "name": "Professional", "tier_position": 1, "sort_order": 1 } ], "entitlements": [ { "unique_handle": "data_export", "name": "Data Export", "type": "feature" } ], "entitlement_rules": [ { "id": "er_data_export_pro", "entitlement_id": "data_export", "targets": [{ "kind": "plan", "id": "professional" }], "segment_ids": [], "kind": "feature", "enabled": true } ], "segments": [], "content_ui_paths": [], "surface_templates": [ { "id": "modal_overlay", "surface_type": "modal", "fields": [] } ], "placements": [ { "id": "pl_export_gate", "name": "Export gate", "category": "gated", "trigger": { "type": "entitlement_gate", "entitlement_handle": "data_export" }, "payloads": [ { "id": "pl_export_gate_p0", "target": { "plan_ids": ["free"], "segment_chips": [] }, "surfaces": [ { "template_id": "modal_overlay", "fields": { "modal_type": "Blocking", "header": "Unlock Data Export", "body": "Video export is available on the Professional plan. Upgrade to export your projects in MP4, WebM, and GIF formats." }, "ctas": [ { "label": "View Plans", "path": "open_checkout", "config": { "purchase": "professional" } }, { "label": "Maybe later", "path": "dismiss" } ] } ], "caps": {}, "status": "active" } ], "order": 0 } ] } ``` 2. **Create the gated component** ```tsx // src/components/ExportPanel.tsx import { Gate } from '@revturbine/sdk'; function ExportControls() { return (

Export Settings

); } export function ExportPanel() { return ( {/* This only renders when the entitlement is granted */} ); } ``` 3. **Use it in your page** ```tsx // src/pages/Editor.tsx import { ExportPanel } from '../components/ExportPanel'; export default function Editor() { return (

Video Editor

{/* ... editor UI ... */}

Export

); } ``` 4. **Test with different users** ```tsx // Free user → sees upgrade modal ; // Professional user → sees export controls ; ``` ## How It Works The `` component: 1. Checks the `data_export` entitlement against the user's plan 2. If **granted** → renders the children (`ExportControls`) 3. If **denied** → resolves the gate placement and renders the upgrade modal The gate check happens instantly in `local_only` mode using the Playbook. ## Alternative: Hook-Based Approach For more control, use the `useEntitlement` hook directly: ```tsx import { useEntitlement, usePlacement } from '@revturbine/sdk'; function ExportPanel() { const { allowed, denied } = useEntitlement({ handle: 'data_export', }); const { visible, content, dismiss, ctaClick } = usePlacement({ surfaceSlot: { id: 'editor_export_action', surfaceTemplateIds: ['modal_overlay'], }, autoLoad: denied, // Only load placement if denied }); if (allowed) return ; if (visible) { return (

{content?.header}

{content?.body}

); } return ; } ``` ## Try It Live > [Interactive Playground → Data Export Gate](/revturbine-sdk/guides/entitlements/#gate-modal) ## Next Steps - [Tutorial: Track Usage and Show a Quota Meter](/revturbine-sdk/tutorials/usage-quota-meter/) — usage-based entitlements - [Entitlements Guide](/revturbine-sdk/guides/entitlements/) — all entitlement types and patterns # Tutorial: Warn Users About Low Credits > End-to-end tutorial — show a running credit balance and surface a warning when a user's credits run low, with a top-up CTA. In this tutorial, you'll display a user's remaining **credits** and surface a warning placement when the balance runs low — at 80% consumed — so users top up before they're blocked. Takes about 15 minutes. ## What You'll Build A credits flow that: - Reports credit consumption to the SDK with `update()` - Reads the remaining balance with `useEntitlement()` - Fires a low-credits warning placement when consumption crosses 80% ## Prerequisites - A React 18+ application with `@revturbine/sdk` installed - A `RevTurbineProvider` set up (see [Quickstart](/revturbine-sdk/)) ## Steps 1. **Add a credits entitlement and a low-credits placement** A `credits` entitlement with a per-plan allowance, plus a `usage_credit_seat` placement triggered by `credit_threshold` at 80%. This is a complete, valid `Playbook`. ```json title="revturbine.playbook.json" { "artifact_type": "playbook", "format_version": "1.0.0", "plans": [ { "unique_handle": "free", "name": "Free", "tier_position": 0, "sort_order": 0 }, { "unique_handle": "pro", "name": "Pro", "tier_position": 1, "sort_order": 1 } ], "entitlements": [ { "unique_handle": "credits", "name": "Render Credits", "type": "credits", "unit": "credits" } ], "entitlement_rules": [ { "id": "er_credits_free", "entitlement_id": "credits", "targets": [{ "kind": "plan", "id": "free" }], "segment_ids": [], "kind": "credits", "allowance_value": 20, "rollover_enabled": false, "max_balance": null } ], "segments": [], "content_ui_paths": [], "surface_templates": [ { "id": "banner_placement", "surface_type": "banner", "fields": [] } ], "placements": [ { "id": "pl_credits_low", "name": "Low credits warning", "category": "usage_credit_seat", "trigger": { "type": "credit_threshold", "entitlement_handle": "credits", "threshold_percent": 80 }, "payloads": [ { "id": "pl_credits_low_p0", "target": { "plan_ids": ["free"], "segment_chips": [] }, "surfaces": [ { "template_id": "banner_placement", "fields": { "header": "{{usage_remaining}} credits left", "body": "Top up to keep rendering.", "dismissible": "Yes" }, "ctas": [ { "label": "Buy credits", "path": "open_checkout", "config": { "purchase": "credit_pack" } } ] } ], "caps": { "max_per_period": { "count": 1, "period": "day" } }, "status": "active" } ], "order": 0 } ] } ``` 2. **Report credit usage as it's consumed** Each time a user spends credits, report the new running total. This re-evaluates the warning's threshold. ```tsx title="src/hooks/useRender.ts" import { useRevTurbine } from '@revturbine/sdk'; import { useCallback, useRef } from 'react'; export function useRender() { const { sdk } = useRevTurbine(); const spent = useRef(0); return useCallback((cost: number) => { spent.current += cost; sdk?.update({ usage: { credits: spent.current } }); }, [sdk]); } ``` 3. **Show the remaining balance** Read the live balance with `useUsageSnapshot` — the `credits` entry reflects every `update()` call. ```tsx title="src/components/CreditBadge.tsx" import { useUsageSnapshot } from '@revturbine/sdk'; export function CreditBadge() { const { usage } = useUsageSnapshot(); const credits = usage['credits']; // { current, limit? } if (!credits) return null; const remaining = credits.limit != null ? Math.max(0, credits.limit - credits.current) : undefined; const low = credits.limit != null && credits.current / credits.limit >= 0.8; return ( {remaining} / {credits.limit} credits ); } ``` `limited` flips to `true` when the balance is **at or past** the allowance — not as it approaches one. There is no "approaching" state: to warn before the cap, compare `current` against `limit` yourself (the placement below triggers at 80%). And read `allowed` before granting access — under the default enforcement a `limited` result is `allowed: false`. 4. **Render the warning slot** Place the slot where the banner should appear. It stays hidden until consumption crosses 80%, then renders the warning with the balance interpolated into the copy via `{{usage_remaining}}`. ```tsx title="src/components/AppHeader.tsx" import { Slot } from '@revturbine/sdk'; export function AppHeader() { return (
); } ``` ## How It Works 1. `update()` updates the SDK's credit balance and re-evaluates placements. 2. The `credit_threshold` trigger keeps the placement hidden until 80% of the allowance is consumed. 3. Token expansion fills `{{usage_remaining}}` from the live balance. 4. The CTA opens your top-up flow through the resolvers from [Quickstart](/revturbine-sdk/). **Caution:** Credits are revenue-critical. In `local_only` mode the balance is client-side and advisory. Before charging for a top-up or letting a user spend credits they may not have, confirm the balance server-side with `can` — see the [Entitlements guide](/revturbine-sdk/guides/entitlements/). ## Try It Live > [Interactive Playground → Message Banner](/revturbine-sdk/playground/#msg-banner) ## Next Steps - [Entitlements Guide](/revturbine-sdk/guides/entitlements/) — credits, usage limits, and the `limited` state - [Tutorial: Track Usage and Show a Quota Meter](/revturbine-sdk/tutorials/usage-quota-meter/) — the usage-limit equivalent - [Component Gallery](/revturbine-sdk/components/) — every built-in slot component # Tutorial: Recover a Failed Payment > End-to-end tutorial — show a persistent, non-dismissible banner that prompts users to fix a failed payment before they lose access. In this tutorial, you'll surface a **payment recovery** banner to users whose last charge failed — a persistent, non-dismissible prompt to update their payment method before their subscription lapses. Takes about 10 minutes. ## What You'll Build A dunning banner that: - Targets only users in a failed-payment state via a segment - Stays visible (non-dismissible) until the issue is resolved - Links to your billing portal to update the card on file ## Prerequisites - A React 18+ application with `@revturbine/sdk` installed - A `RevTurbineProvider` set up (see [Quickstart](/revturbine-sdk/)) - A way to know a user's billing status (e.g. a Stripe webhook updating your backend) ## Steps 1. **Add a billing-failed segment and a recovery placement** A segment matches users whose `billing_status` is `payment_failed`; the `retention` placement targets that segment and is non-dismissible. This is a complete, valid `Playbook`. ```json title="revturbine.playbook.json" { "artifact_type": "playbook", "format_version": "1.0.0", "plans": [], "entitlements": [], "entitlement_rules": [], "segments": [ { "id": "seg_billing_failed", "name": "Payment failed", "handle": "seg_billing_failed", "predicates": [ { "field": "billing_status", "operator": "eq", "value": "payment_failed" } ] } ], "content_ui_paths": [], "surface_templates": [ { "id": "banner_placement", "surface_type": "banner", "fields": [] } ], "placements": [ { "id": "pl_payment_recovery", "name": "Payment recovery", "category": "retention", "trigger": { "type": "qualifier", "qualifier": "payment_failed" }, "payloads": [ { "id": "pl_payment_recovery_p0", "target": { "plan_ids": [], "segment_chips": ["seg_billing_failed"] }, "surfaces": [ { "template_id": "banner_placement", "fields": { "header": "Your last payment failed", "body": "Update your payment method to keep your subscription active.", "dismissible": "No" }, "ctas": [ { "label": "Update payment", "path": "update_payment_method" } ] } ], "caps": {}, "status": "active" } ], "order": 0 } ] } ``` 2. **Pass the user's billing status into the SDK** The placement's `payment_failed` trigger reads the user context's `payment_failed` flag, and its segment reads `billing_status`. Feed both from your backend, and update them whenever your payment provider's webhook fires. ```tsx title="src/App.tsx" ``` 3. **Render the banner slot** ```tsx title="src/components/AppHeader.tsx" import { Slot } from '@revturbine/sdk'; export function AppHeader() { return (
{/* Visible only while billing_status is payment_failed; non-dismissible */}
); } ``` 4. **Clear it on recovery** When the payment succeeds, clear the flag and set `billing_status` back to `active`; the banner stops rendering automatically — no manual teardown. ```ts sdk.update({ payment_failed: false, custom: { billing_status: 'active' } }); ``` ## How It Works 1. The `payment_failed` qualifier trigger fires while the user context's `payment_failed` flag is `true`. 2. The `seg_billing_failed` segment matches users whose `billing_status` context equals `payment_failed`, and the placement targets that segment via `segment_chips`, so only affected users see the banner. 3. `"dismissible": "No"` keeps it visible — dunning prompts shouldn't be dismissible. 4. Clearing the flag and setting `billing_status` to `active` takes the user out of both, and the banner disappears on the next evaluation. **Caution:** The SDK targets and renders the banner; it doesn't process payments. Drive `billing_status` from your billing provider's source of truth (e.g. a Stripe `invoice.payment_failed` / `invoice.paid` webhook) so the banner reflects reality. ## Try It Live > [Interactive Playground → Banner Slot](/revturbine-sdk/playground/#fixed-banner) ## Next Steps - [Placements Guide](/revturbine-sdk/guides/placements/) — segments, targeting, and triggers - [Tutorial: Show a Banner Placement](/revturbine-sdk/tutorials/banner-placement/) — targeted banners - [Tutorial: Show a Trial-Ending Countdown](/revturbine-sdk/tutorials/trial-ending-nudge/) — another lifecycle nudge # Tutorial: Show a Trial-Ending Countdown > End-to-end tutorial — surface a countdown banner in the final days of a trial to convert users before it expires. In this tutorial, you'll show a **trial-ending** banner that counts down the final days of a user's trial and prompts them to upgrade before it expires. Takes about 10 minutes. ## What You'll Build A trial countdown that: - Fires only in the final window of a trial (the last 3 days) - Renders the days remaining via the `{{days_remaining}}` token - Links to checkout with an upgrade CTA ## Prerequisites - A React 18+ application with `@revturbine/sdk` installed - A `RevTurbineProvider` set up (see [Quickstart](/revturbine-sdk/)) ## Steps 1. **Add a trial-ending placement to your config** The `trial_ending` trigger fires when a trial enters its closing window. This is a complete, valid `Playbook`. ```json title="revturbine.playbook.json" { "artifact_type": "playbook", "format_version": "1.0.0", "plans": [ { "unique_handle": "pro", "name": "Pro", "tier_position": 1, "sort_order": 1 } ], "entitlements": [], "entitlement_rules": [], "segments": [], "content_ui_paths": [], "surface_templates": [ { "id": "banner_placement", "surface_type": "banner", "fields": [] } ], "placements": [ { "id": "pl_trial_ending", "name": "Trial ending countdown", "category": "trials", "trigger": { "type": "trial_ending", "days_before_end": 3 }, "payloads": [ { "id": "pl_trial_ending_p0", "target": { "plan_ids": [], "segment_chips": [] }, "surfaces": [ { "template_id": "banner_placement", "fields": { "header": "Your trial ends in {{days_remaining}} days", "body": "Upgrade now to keep your projects and Pro features.", "dismissible": "Yes" }, "ctas": [ { "label": "Upgrade to Pro", "path": "open_checkout", "config": { "purchase": "pro" } } ] } ], "caps": { "max_per_period": { "count": 1, "period": "day" } }, "status": "active" } ], "order": 0 } ] } ``` 2. **Provide trial status to the SDK** In local mode the SDK reads trial state from `localRuntime.initialData` (in hosted mode it arrives with the user context). Supply the trial so the trigger and `{{days_remaining}}` token can compute the window. ```tsx title="src/App.tsx" ``` 3. **Render the banner slot** ```tsx title="src/components/AppHeader.tsx" import { Slot } from '@revturbine/sdk'; export function AppHeader() { return (
{/* Hidden until the trial enters its final 3 days */}
); } ``` 4. **(Optional) Read trial status directly** To drive your own UI — a sidebar countdown, a settings notice — read trial status imperatively: ```tsx import { useEffect, useState } from 'react'; import { useRevTurbine } from '@revturbine/sdk'; function TrialNotice() { const { sdk } = useRevTurbine(); const [trial, setTrial] = useState(null); useEffect(() => { if (sdk) void Promise.resolve(sdk.getTrialStatus()).then(setTrial); }, [sdk]); if (!trial?.in_trial) return null; return

{trial.days_remaining} days left in your trial

; } ``` ## How It Works 1. The `trial_ending` trigger keeps the placement hidden until the trial is within `days_before_end` of expiring. 2. `{{days_remaining}}` is filled from the trial status the SDK holds. 3. As the trial advances day by day, the banner copy updates automatically. 4. The CTA opens checkout through the resolvers from [Quickstart](/revturbine-sdk/). **Tip:** RevTurbine supports both **forward trials** (paid features for a limited time) and **reverse trials** (full access that downgrades to free). The same trigger pattern applies — only the trial status the SDK reports differs. ## Try It Live > [Interactive Playground → Banner Slot](/revturbine-sdk/playground/#fixed-banner) ## Next Steps - [Entitlements Guide](/revturbine-sdk/guides/entitlements/) — trials, usage limits, and credits - [Placements Guide](/revturbine-sdk/guides/placements/) — triggers and targeting - [Tutorial: Show a Banner Placement](/revturbine-sdk/tutorials/banner-placement/) — targeted banners without a trigger # Tutorial: Add an Upgrade Button > End-to-end tutorial — install the SDK, configure local mode, and place an upgrade button that renders based on targeting rules. In this tutorial, you'll install the SDK, configure it in local mode, and render an upgrade button that only appears for users on the Starter plan. Takes about 10 minutes. ## What You'll Build An upgrade button in your navigation bar that: - Shows "Upgrade to Pro" for Starter plan users - Hides automatically for Professional/Enterprise users - Tracks impressions and CTA clicks - Navigates to your pricing page on click ## Prerequisites - A React 18+ application (Next.js, Vite, or CRA) - Node.js 22.13+ ## Steps 1. **Install the SDK** ```bash pnpm add @revturbine/sdk ``` 2. **Create an Playbook fixture** For local development, you need a Playbook with plans, entitlements, and placements. Create `src/fixtures/revturbine.playbook.json`: ```json title="revturbine.playbook.json" { "artifact_type": "playbook", "format_version": "1.0.0", "plans": [ { "unique_handle": "starter", "name": "Starter", "tier_position": 0, "sort_order": 0 }, { "unique_handle": "pro", "name": "Pro", "tier_position": 1, "sort_order": 1 } ], "entitlements": [], "entitlement_rules": [], "segments": [], "content_ui_paths": [], "surface_templates": [ { "id": "button", "surface_type": "button", "fields": [] } ], "placements": [ { "id": "pl_nav_upgrade", "name": "Upgrade button", "category": "fixed", "trigger": { "type": "surface_render", "slot_id": "nav_upgrade" }, "payloads": [ { "id": "pl_nav_upgrade_p0", "target": { "plan_ids": ["starter"], "segment_chips": [] }, "surfaces": [ { "template_id": "button", "fields": {}, "ctas": [ { "label": "Upgrade to Pro", "path": "open_checkout", "config": { "purchase": "pro" } } ] } ], "caps": {}, "status": "active" } ], "order": 0 } ] } ``` 3. **Wrap your app with the provider** ```tsx // src/App.tsx import { RevTurbineProvider, RuntimeMode } from '@revturbine/sdk'; import playbook from './fixtures/revturbine.playbook.json'; export default function App() { return ( ); } ``` 4. **Add the upgrade button slot** ```tsx // src/components/NavBar.tsx import { Slot } from '@revturbine/sdk'; export function NavBar() { return ( ); } ``` 5. **Verify it works** Start your dev server and you should see: - **Starter user** → "Upgrade to Pro" button appears - **Professional user** → nothing renders (slot is invisible) To test different users, change the user context in the provider: ```tsx ``` ## What Just Happened 1. The `RevTurbineProvider` initialized the SDK in `local_only` mode with your Playbook 2. The `` asked the decision engine for a placement at slot `nav_upgrade` 3. The engine evaluated the payload target (`plan_ids: ['starter']`) against the user context 4. For Starter users, it resolved the `button` template with the "Upgrade to Pro" CTA 5. For Pro users, no payload matched — the slot rendered nothing ## Local mode is production-ready `local_only` is not a demo mode you graduate out of. It evaluates the full Playbook client-side and is a valid production runtime on its own — ship it as-is when a bundled Playbook, client-side decisioning, and `localStorage` events are enough for you. Switch to the RevTurbine **server** runtime when you want what it *adds*, not because local mode isn't "real": live Playbook updates without a redeploy, server-authoritative usage and metering, A/B experiments, and centralized analytics through the ingest pipeline. See [Runtime Modes](/revturbine-sdk/guides/runtime-modes/) for the full comparison. To switch, point the provider at your tenant and drop the local runtime: ```diff ``` ## Try It Live > [Interactive Playground → Upgrade Button](/revturbine-sdk/guides/placements/#fixed-button) ## Next Steps - [Tutorial: Gate a Premium Feature](/revturbine-sdk/tutorials/gate-premium-feature/) — add an entitlement gate - [Component Gallery](/revturbine-sdk/components/) — interactive demos of all built-in slot components # Tutorial: Track Usage and Show a Quota Meter > End-to-end tutorial — report usage, check limits, and render a quota meter that updates in real time. In this tutorial, you'll report API usage to the SDK, display a quota meter that updates in real time, and trigger a warning placement when usage exceeds 80%. Takes about 15 minutes. ## What You'll Build A usage tracking flow that: - Reports API call counts to the SDK via `update()` - Shows a quota meter with current/limit display - Triggers a warning banner when usage exceeds 80% - Triggers an upgrade modal when usage exceeds 100% ## Prerequisites - A React 18+ application with `@revturbine/sdk` installed - A `RevTurbineProvider` set up (see [Quickstart](/revturbine-sdk/)) ## Steps 1. **Add usage entitlements and placements to your config** ```json title="revturbine.playbook.json" { "artifact_type": "playbook", "format_version": "1.0.0", "plans": [ { "unique_handle": "starter", "name": "Starter", "tier_position": 0, "sort_order": 0 }, { "unique_handle": "professional", "name": "Professional", "tier_position": 1, "sort_order": 1 } ], "entitlements": [ { "unique_handle": "api_calls", "name": "API Calls", "type": "usage_limit", "unit": "calls" } ], "entitlement_rules": [ { "id": "er_api_calls_starter", "entitlement_id": "api_calls", "targets": [{ "kind": "plan", "id": "starter" }], "segment_ids": [], "kind": "usage_limit", "limit_value": 1000, "unit": "calls", "period_scope": "per_month", "enforcement": "hard_block" } ], "segments": [], "content_ui_paths": [], "surface_templates": [ { "id": "usage_counter", "surface_type": "in_page", "fields": [] }, { "id": "banner_warning", "surface_type": "banner", "fields": [] } ], "placements": [ { "id": "pl_api_quota_meter", "name": "API quota meter", "category": "fixed", "trigger": { "type": "surface_render", "slot_id": "sidebar_usage" }, "payloads": [ { "id": "pl_api_quota_meter_p0", "target": { "plan_ids": [], "segment_chips": [] }, "surfaces": [ { "template_id": "usage_counter", "fields": { "header": "API Calls", "body": "{{usage_current}} / {{usage_limit}} calls used" }, "ctas": [] } ], "caps": {}, "status": "active" } ], "order": 0 }, { "id": "pl_usage_warning", "name": "Usage limit approaching", "category": "usage_credit_seat", "trigger": { "type": "usage_threshold", "entitlement_handle": "api_calls", "threshold_percent": 80 }, "payloads": [ { "id": "pl_usage_warning_p0", "target": { "plan_ids": ["starter"], "segment_chips": [] }, "surfaces": [ { "template_id": "banner_warning", "fields": { "header": "Usage limit approaching", "body": "You've used {{usage_percent}}% of your API calls this month.", "dismissible": "Yes" }, "ctas": [ { "label": "View Plans", "path": "open_checkout", "config": { "purchase": "professional" } } ] } ], "caps": {}, "status": "active" } ], "order": 1 } ] } ``` 2. **Add the quota meter to your sidebar** ```tsx // src/components/Sidebar.tsx import { Slot } from '@revturbine/sdk'; export function Sidebar() { return ( ); } ``` 3. **Add the warning banner** ```tsx // src/components/DashboardLayout.tsx import { Slot } from '@revturbine/sdk'; export function DashboardLayout({ children }) { return (
{/* Warning banner appears when usage > 80% */}
{children}
); } ``` 4. **Report usage from your API layer** ```tsx // src/hooks/useApiCall.ts import { useRevTurbine } from '@revturbine/sdk'; import { useCallback, useRef } from 'react'; export function useApiCall() { const { sdk } = useRevTurbine(); const callCount = useRef(0); const apiCall = useCallback(async (endpoint: string, options?: RequestInit) => { const response = await fetch(endpoint, options); // Report usage to the SDK callCount.current += 1; sdk?.update({ usage: { api_calls: callCount.current } }); return response; }, [sdk]); return { apiCall }; } ``` 5. **Test the flow** Simulate increasing usage to see the meter and banner update: ```tsx function DemoControls() { const { sdk } = useRevTurbine(); const [calls, setCalls] = useState(0); const simulateUsage = (count: number) => { setCalls(count); sdk?.update({ usage: { api_calls: count } }); }; return (

Demo: Simulate API Usage

Current: {calls} calls

); } ``` **Expected behavior:** | Usage | Quota Meter | Warning Banner | |---|---|---| | 500 / 1000 (50%) | Green bar, "500 / 1000" | Hidden | | 800 / 1000 (80%) | Yellow bar, "800 / 1000" | Shows "Usage limit approaching" | | 950 / 1000 (95%) | Red bar, "950 / 1000" | Shows "Usage limit approaching" | | 1000 / 1000 (100%) | Full red bar | Still shows; `can('api_calls')` now denies (`hard_block`) | ## How It Works 1. `update()` updates the SDK's internal usage balances 2. The decision engine re-evaluates all active placements 3. The quota meter refreshes its display (current/limit) 4. When usage exceeds the targeting threshold (80%), the warning banner becomes visible 5. Token expansion fills in `{{usage_current}}`, `{{usage_limit}}`, `{{usage_percent}}` ## Triggering Programmatic Alerts For more control, emit trigger events: ```ts // When usage approaches limit if (usagePercent >= 80) { await sdk.emitTrigger('usage_limit_approaching', { entitlement_handle: 'api_calls', current_usage: currentCalls, usage_limit: 1000, usage_percent: usagePercent, }); } // When usage reaches the limit if (usagePercent >= 100) { await sdk.emitTrigger('usage_limit_reached', { entitlement_handle: 'api_calls', current_usage: currentCalls, usage_limit: 1000, }); } ``` ## Try It Live > [Interactive Playground → Quota Meter](/revturbine-sdk/playground/#fixed-usage-counter) ## Next Steps - [Entitlements Guide](/revturbine-sdk/guides/entitlements/) — all entitlement types - [Events & Analytics](/revturbine-sdk/guides/events/) — trigger events and tracking - [Component Gallery](/revturbine-sdk/components/) — interactive demos of all built-in slot components # Protect a Cloud Export > Authenticate UserContext, load a Playbook, and enforce a paid action with a fresh SDK session. This tutorial starts with a Vercel standalone Edge Function. The same application handler also has Node, Firebase, AWS, Google Cloud, and Azure adapters in the [cloud examples](/revturbine-sdk/guides/cloud-entitlements/). ## 1. Run the example locally Clone the [SDK repository](https://github.com/revt-eng/revturbine-sdk) and open `examples/cloud-entitlements`. Install with `npm ci`, check types with `npm run typecheck`, and run `npm test`. Tests provide their own keys and delivery fixtures. You do not need to deploy or supply production credentials. The fixture grants the boolean `data_export` entitlement to `pro` and denies it to `free`. Tests verify both decisions, rejected authentication, unavailable configuration, cache refresh, and user isolation against the published SDK. ## 2. Establish authoritative context In your trusted application backend, authenticate the caller and resolve their account membership and plan from your application's records. Pass that context to `issuer/issue-context-token.ts`. Keep its signing private key on the issuer; publish only public keys at your HTTPS JWKS URL. Set `CONTEXT_JWKS_URL`, `CONTEXT_ISSUER`, and `CONTEXT_AUDIENCE` to your application's values. The token's subject must match `user.id`, and its tenant must match the deployment. The helper is not an endpoint for signing arbitrary browser JSON. This application JWT is separate from an `rt_client_` RevTurbine client-session token. Do not send a client-session token to this verifier or treat the two credential types as interchangeable. ## 3. Configure trusted Playbook delivery Use the variable names in `.env.example`. Set `REVTURBINE_TENANT_ID`, the secret server `REVTURBINE_API_KEY`, `REVTURBINE_ENDPOINT`, and an explicit allowlist in `REVTURBINE_CDN_ORIGINS`. Use the actual delivery origins supplied by your deployment; do not accept an origin from the incoming request. `src/playbook-cache.ts` shares only immutable snapshots. It authenticates bootstrap, checks scope/integrity/version/expiry, and refreshes within 60 seconds. The cache deliberately rejects a failed refresh instead of serving stale grants. The native connected alternative is in `src/native-connected-example.ts`. The four-second delivery timeout is per fetch, not for the complete operation. JWKS resolution separately allows three seconds. Account for sequential requests when setting your overall application deadline and provider timeout. ## 4. Follow the enforcement boundary `api/check-export.ts` selects the Edge runtime and delegates to the configured handler. `src/check-export.ts` verifies authentication before loading policy, initializes a fresh SDK session, invokes `session.can('data_export')`, and disposes the session in `finally`. There is no shared mutable session for multiple users. The sample returns 200 for allowed, 403 for denied, 401 for rejected authentication, 405 for an unsupported method, and 503 for unavailable configuration. Personalized responses carry `private, no-store`. Replace the allowed response with your export operation, after verifying that the caller owns or may access the requested resource. Do not call this endpoint from the browser and treat a successful response as authorization for a separate, unguarded download. Keep the check and protected operation in the trusted handler. ## 5. Choose your runtime Use `api/check-export-node.ts` when you need Node APIs or your framework does not support standalone Edge execution. Keep Node-only provider imports outside the Edge graph. Other providers use the same handler through the adapters listed in the [cloud guide](/revturbine-sdk/guides/cloud-entitlements/). Before optional deployment, follow the example README's platform-specific setup. Disable CDN caching for the route, forward Authorization, protect direct origin access, and store server secrets in the provider's secret facility. Firebase, Google Cloud, and Azure CDN front doors still execute this handler regionally. ## 6. Decide freshness and accounting requirements Short-lived context tokens can remain valid until their expiry, and policy snapshots can remain current for the cache interval. Document those bounds for your application. Add revocation checks when changes must take effect sooner. The example trusts authenticated HTTPS bootstrap and checks the supplied digest; it does not independently verify optional manifest signatures. For a metered export, use atomic application-side quota accounting as well as the SDK policy check. Telemetry delivery and an entitlement decision are not a durable usage transaction. See [server-side integration](/revturbine-sdk/getting-started/server-side/) for session lifecycle and event-delivery details. # RevTurbineUserContext > Identify users and keep plan, usage, traits, experiments, and server-derived signals current. `RevTurbineUserContext` is the user snapshot RevTurbine evaluates against the active Playbook. Entitlement checks, Segment targeting, placements, personalization, and experiment assignment all depend on this context. For most integrations, start with a stable user id and the current plan handle. Add only the fields your Playbook reads. ```ts import type { RevTurbineUserContext } from '@revturbine/sdk'; export const user: RevTurbineUserContext = { id: 'usr_01J8M4X9Q2', plan_handle: 'starter', custom: { role: 'editor', company_size: 42, }, }; ``` **Caution — User context is flat:** There is no nested `context` wrapper. `plan_handle` is a direct property of the user object. ```diff - user: { id: 'user_123', context: { plan_handle: 'pro' } } + user: { id: 'user_123', plan_handle: 'pro' } ``` The TypeScript context types reject unknown top-level keys. `identify()` and `update()` also warn and drop unknown keys from plain JavaScript callers. Put customer-defined fields under `custom`. ## Choose the right entry point | Situation | API | Behavior | |---|---|---| | Initialize React or the browser SDK | `options.user` | Sets the initial `RevTurbineUserContext` | | A user logs in or you switch accounts | `identify(userId, context?)` | Establishes the identity, invalidates cached decisions, and applies identity-safe context fields | | Data changes for the same user | `update(patch)` | Merges plan, usage, custom traits, and other supported session fields without changing identity | | Merge a low-level context snapshot | `setUserContext(context)` | Merges a `RevTurbineUserContext`; React's `setContext` calls this method | | A user logs out | `reset()` | Clears the known identity and returns to inferred anonymous state | | A demo or test needs a blank slate | `resetUserContext()` | Clears all user values, usage, decisions, and interaction history without anonymous inference | | Inspect held SDK state | `getUserContext()` | Returns the current context with `tenant_id`, `user_id`, and persistence-envelope fields | | Inspect what decisions actually read | `getEffectiveUserContext()` | Returns the immutable context after customer and experiment providers resolve | Use `identify()` whenever the identity changes. Use `update()` for later changes to the same user: ```ts import type { RevTurbineCustomerSdk } from '@revturbine/sdk'; type SignedInUser = { id: string; accountId: string; planHandle: string; role: string; }; export function syncSignedInUser(rt: RevTurbineCustomerSdk, user: SignedInUser) { rt.identify(user.id, { account_id: user.accountId, plan_handle: user.planHandle, custom: { role: user.role }, }); } export function applyPlanChange(rt: RevTurbineCustomerSdk, planHandle: string) { rt.update({ plan_handle: planHandle }); } export function signOut(rt: RevTurbineCustomerSdk) { rt.reset(); } ``` Context updates notify mounted hooks and components, so active entitlement checks and placements re-evaluate for the new snapshot. The optional context passed to `identify()` accepts `account_id`, `email`, `plan_handle`, `plan`, `usage`, `entitlements`, `custom`, and `personalization`. Set trial, tier, billing-recovery, and instance changes at initialization or with `update()`. Supply direct `experiments` assignments at initialization, through an experiment provider, or through the lower-level `setUserContext()` merge. ## Field reference ### Identity and account | Field | Type | Guidance | |---|---|---| | `id` | `string` | Your stable, opaque user identifier. Supply it in `options.user`; pass it as the first argument to `identify()`. Avoid emails and other guessable identifiers | | `account_id` | `string \| null` | The organization, workspace, or account the user is acting in. Optional — leave it unset and every event the SDK emits carries `user-fallback:` instead. That prefix is the point: `account_id` is the join key RevTurbine's account-grain analytics match on, so a *bare* user id stamped there is indistinguishable from a real account, while a prefixed one is countable and obviously fabricated (RevTurbine's own account-grain readouts exclude it). Use `isFallbackAccountId()` to classify a value. Set `account_id` whenever your product has a notion of an account | | `email` | `string \| null` | Optional contact identity. Put email here, never in `id` | | `email_type` | `string` | Optional classification such as `business` or `personal` | `id` is optional at the type boundary because the browser SDK can operate with anonymous state. Once your app knows the user, identify them with the same stable internal id on every device and session. ### Plan | Field | Type | Guidance | |---|---|---| | `plan_handle` | `string` | Preferred plan identity. It must equal the plan's `unique_handle` in the Playbook, such as `starter` or `pro` | | `plan` | `{ handle, name, price?, billing_period? }` | Optional display metadata. `plan.handle` is a fallback identity when `plan_handle` is absent; the flat field wins when both are present | Plan ids from your database or RevTurbine's internal records do not belong in user context. `plan.id` is a retired shape and is rejected. Use the plan's stable handle: ```ts import type { RevTurbineUserContext } from '@revturbine/sdk'; export const professionalUser: RevTurbineUserContext = { id: 'usr_01J8M4X9Q2', plan_handle: 'pro', plan: { handle: 'pro', name: 'Professional', price: '$49', billing_period: 'monthly', }, }; ``` ### Decision and presentation inputs | Field | Type | What reads it | |---|---|---| | `custom` | `Record` | Customer-owned attributes for Segment targeting, such as role, region, or company size. Use string, number, or boolean values for predicates | | `usage` | `Record` | Usage-limit and credit entitlements. Keys are entitlement handles; see [Report usage](#report-usage) | | `trial` | `RevTurbineTrialContext` | Reverse-trial entitlement grants, trial-lifecycle placements (`trial_ending`, `trial_progress`, `trial_ended`, `trial_converted`) and the `{{trial_days_remaining}}` / `{{trial_days_total}}` tokens. When a server-delivered or hydrated trial status is also present, the most recent change wins | | `tiers` | `Record` | Current tier handle per capability-tier entitlement handle | | `entitlements` | `Record` | Precomputed grants. Most apps should let the Playbook derive these rather than asserting them in the browser | | `experiments` | `Record` | Experiment handle to variant handle assignments; see [Use Experiment Assignments](/revturbine-sdk/guides/experiments/) | | `payment_failed` | `boolean` | Hard-failure recovery placements | | `payment_at_risk` | `boolean` | Soft billing-attention placements | | `instances` | `UserInstanceContext[]` | Per-product-instance plans, usage, trials, and entitlements for multi-instance accounts | | `personalization` | `Record` | Transient tokens used in placement copy. It is rebuilt for each SDK session and is not an access-control input | Browser values can be changed by the person running the browser. Use them for UI decisions, but re-run value-bearing entitlement checks in a server SDK over the same Playbook before spending money, provisioning seats, or granting sensitive access. See [Client vs Server Enforcement](/revturbine-sdk/concepts/enforcement/). ## Server SDK context shapes The TypeScript headless entry point accepts this same canonical `RevTurbineUserContext`. The Python and Rust local evaluators instead expose smaller, request-scoped `UserContext` facade types. Map your application user into the shape for the SDK you are calling: | Concept | TypeScript headless | Python and Rust server facade | |---|---|---| | Identity | `user.id`; tenant is the top-level `tenantId` initialization option | `tenant_id` and `user_id` inside the facade object | | Plan | `plan_handle`, with optional `plan: { handle, name, ... }` metadata | `plan_handle`, with optional `plan_name`; no `plan` object | | Usage at construction | `{handle: {entitlement_handle, unit, amount, limit?}}` | `{handle: {used, limit}}` | | Trial | `trial` | `trial_status`, containing already-derived trial state | | Targeting | `custom`, `experiments`, and other canonical context fields | No raw-trait or experiment derivation; Python also accepts pre-resolved `segment_ids` for eligible plan and add-on helpers | | Shared signals | `payment_failed`, `payment_at_risk`, and `tiers` | The same three fields | **Caution — Use the facade type, not the persisted model:** For Python, import `UserContext` from `revturbine`. For Rust, import it from `revturbine::sdk`. Do not pass the generated persisted models `revturbine.types.UserContext` or `revturbine::types::UserContext` to the local evaluator constructor. Those models include database envelope fields and use the broader canonical schema; they are not constructor inputs. See the detailed [TypeScript server guide](/revturbine-sdk/getting-started/server-side/), [Python guide](/revturbine-sdk/getting-started/python/), and [Rust guide](/revturbine-sdk/getting-started/rust/). ## Report usage Initial user context uses rich `UserUsageEntry` objects. The record key and `entitlement_handle` both refer to the entitlement's `unique_handle`: ```ts import type { RevTurbineUserContext } from '@revturbine/sdk'; export const userWithUsage: RevTurbineUserContext = { id: 'usr_01J8M4X9Q2', plan_handle: 'pro', usage: { api_calls: { entitlement_handle: 'api_calls', unit: 'calls', amount: 42, limit: 1_000, }, }, }; ``` Runtime updates use the simpler `Record` shape: ```ts import type { RevTurbineCustomerSdk } from '@revturbine/sdk'; export function reportApiCallTotal(rt: RevTurbineCustomerSdk, total: number) { rt.update({ usage: { api_calls: total } }); } ``` Usage numbers are **absolute balances, not deltas**. If the user has made 43 API calls, report `43`, not `1`. A usage key that does not match an entitlement handle is stored but cannot affect a rule; once a Playbook is loaded, the SDK warns about unmatched keys. ## Merge behavior Context changes use upsert semantics: - Omitted fields and fields set to `undefined` keep their current values. - `setUserContext()` merges `custom`, `entitlements`, `usage`, `personalization`, and `experiments` by key; supplied keys win. - `update()` applies the same merge behavior to its supported non-usage fields and routes its simple numeric `usage` map through the usage update path. - `update()` cannot change `id`. Call `identify(newId, context)` to switch users. - An empty object does not erase a previously populated nested map. Use `reset()` for logout or `resetUserContext()` when you need a completely blank context. Treat the object you pass to RevTurbine as immutable. Create a new object for each meaningful change instead of mutating an earlier value in place. ## Add server-derived context Some signals should come from a trusted backend: the plan last updated by a Stripe webhook, trial status, or coarse payment health. Supply a `clientSession` callback and the SDK will fetch the signed-in user's client-safe context automatically: ```ts import { initRevTurbine } from '@revturbine/sdk'; import { fetchClientSession } from './revturbine-client-session'; export const rt = await initRevTurbine({ publicKey: process.env.NEXT_PUBLIC_REVTURBINE_PUBLIC_KEY!, // public key — safe in the browser user: { id: 'user_123', plan_handle: 'free' }, clientSession: fetchClientSession, // POSTs to your /api/revturbine-session route }); ``` Your backend—not the browser—mints the short-lived `rt_client_…` token. The route and the `fetchClientSession` function are on [Client sessions](/revturbine-sdk/guides/client-sessions/). The SDK requests one when needed, requests a fresh token after `identify()`, and retries with a new token after expiry. Server-derived plan and trial state then join the effective user context used for local evaluation. If enrichment is unavailable, the SDK keeps using the context your app supplied. See [Configuration Reference: server-derived context](/revturbine-sdk/reference/configuration/#server-derived-context-clientsession) for the complete token flow. ## SDK-managed TypeScript fields The broader generated TypeScript user schema also contains persistence and cache metadata. Application code using `RevTurbineUserContext` should not author these fields: - `tenant_id`, `user_id`, `created_at`, and `updated_at` are added by `getUserContext()` and the persistence layer. - `derived_config_version`, `context_hash`, and `derived_computed_at` describe the server-computed entitlement cache. - `activity_score` and `activity_score_computed_at` are server-computed activity signals. Keep application-specific values under `custom`; do not repurpose SDK-managed fields. ## Privacy and validation - Use an opaque internal id. If `identify()` receives an email-shaped id, the SDK warns and tells you to use the `email` field instead. - Do not put secrets, access tokens, payment details, or raw provider records in user context. - Keep Segment-targeted `custom` values scalar. Nested JSON may be accepted at the SDK boundary, but it is not a scalar Segment predicate. - `RevTurbineProvider` options, `identify()`, and `update()` exact-check context objects even when they come through an intermediate variable. Plain JavaScript calls to `identify()` and `update()` receive visible warnings for unknown keys, which are dropped rather than silently becoming traits. ## Related - [Core Concepts](/revturbine-sdk/concepts/core-concepts/) — where user context fits in the decision loop - [React Integration](/revturbine-sdk/getting-started/react/) — provider and hook setup - [Configuration Reference](/revturbine-sdk/reference/configuration/) — initialization options - [Entitlements](/revturbine-sdk/guides/entitlements/) — how plan and usage become access decisions - [Run an Experiment](/revturbine-sdk/guides/experiments/) — direct and provider-owned assignments - [`RevTurbineUserContext` API reference](/revturbine-sdk/api/interfaces/revturbineusercontext/) — generated type details