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](/docs/guides/headless-api/) and [Server-Side Integration](/docs/getting-started/server-side/). Python and Rust SDKs evaluate the same Playbook with byte-identical results. ## 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](/docs/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](/docs/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](/docs/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'; async function handleExport(req, res) { const rt = await initRevTurbine({ tenantId: process.env.REVTURBINE_TENANT_ID!, apiKey: process.env.REVTURBINE_API_KEY!, endpoint: process.env.REVTURBINE_ENDPOINT!, runtimeMode: RuntimeMode.Server, user: { id: req.userId, plan_handle: req.user.planHandle }, }); const result = await rt.can('batch_export'); if (!result.allowed) return res.status(402).json({ error: 'upgrade_required' }); await runExport(req); } ``` Why the re-check, and which actions need it: [Client vs Server Enforcement](/docs/concepts/enforcement/). ## 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. See [Billing & Stripe](/docs/guides/billing/). 3. **Switch the provider to hosted mode.** Mint a publishable ingest key under **Settings → API tokens**, then: ```tsx const options = useMemo(() => ({ tenantId: 'tenant_abc', publicKey: 'rtk_…', // public key — safe in the browser user: { id: currentUser.id }, // plan_handle now comes from Stripe clientSession: () => fetch('/api/revturbine-session', { method: 'POST' }).then((r) => r.json()).then((j) => j.client_token), }), [currentUser.id]); ``` 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](/docs/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](/docs/getting-started/react/#userevturbine) | | Run checks without React | [TypeScript / Node](/docs/guides/headless-api/), [Python](/docs/getting-started/python/), [Rust](/docs/getting-started/rust/) | | Choose Playbook delivery and optional wiring | [Runtime Modes](/docs/guides/runtime-modes/), [Configuration](/docs/reference/configuration/) | | Match your app's colors | [Theming](/docs/guides/theming/) | | Route blocked actions to a paywall | [Paywall Host Pattern](/docs/guides/paywall-host/) | | Explain a missing placement or denied check | [Troubleshooting](/docs/guides/troubleshooting/), [Debugging Decisions](/docs/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](/docs/llms-small.txt) is an abridged documentation bundle, including the language guides; [llms-full.txt](/docs/llms-full.txt) contains the complete documentation. ## Next steps - [Recommended API Path](/docs/getting-started/api-overview/) — `can()` vs `gate()` vs ``, and the vocabulary - [Entitlements](/docs/guides/entitlements/) — feature gates, usage limits, credits, seats, tiers - [Placements](/docs/guides/placements/) — targeting, caps, cooldowns, priority, and UI paths - [Runtime Modes](/docs/guides/runtime-modes/) — local mode, hosted mode, and what is cached - [Interactive Playground](/docs/playground/) — every scenario, live and editable - [API reference](/docs/api/) — full TypeDoc reference for all exports # 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](/docs/api/type-aliases/ctaresolver/) against an action type — typically a tenant-defined custom action name surfaced through `parseUiPath` — so the [PlacementRenderer](/docs/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`](/docs/api/type-aliases/ctaresolver/) \| `undefined` Look up the resolver registered for an action type, if any. #### Parameters ##### type `string` #### Returns [`CtaResolver`](/docs/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`](/docs/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](/docs/api/classes/placementcontroller/#onchange) and read the current state with [state](/docs/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`](/docs/api/interfaces/placementcontrollerstate/) Current state snapshot. ##### Returns [`PlacementControllerState`](/docs/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`](/docs/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](/docs/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](/docs/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`](/docs/api/interfaces/placementslottype/)\<[`PlacementSlotProps`](/docs/api/interfaces/placementslotprops/)\<[`ResolvedContent`](/docs/api/type-aliases/resolvedcontent/)\>\> \| `undefined` Look up a slot type by its id. #### Parameters ##### id `string` #### Returns [`PlacementSlotType`](/docs/api/interfaces/placementslottype/)\<[`PlacementSlotProps`](/docs/api/interfaces/placementslotprops/)\<[`ResolvedContent`](/docs/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`](/docs/api/interfaces/placementslottype/)\<[`PlacementSlotProps`](/docs/api/interfaces/placementslotprops/)\<[`ResolvedContent`](/docs/api/type-aliases/resolvedcontent/)\>\>[] List all registered slot types. #### Returns [`PlacementSlotType`](/docs/api/interfaces/placementslottype/)\<[`PlacementSlotProps`](/docs/api/interfaces/placementslotprops/)\<[`ResolvedContent`](/docs/api/type-aliases/resolvedcontent/)\>\>[] *** ### listByComponentType() > **listByComponentType**(`componentType`): [`PlacementSlotType`](/docs/api/interfaces/placementslottype/)\<[`PlacementSlotProps`](/docs/api/interfaces/placementslotprops/)\<[`ResolvedContent`](/docs/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`](/docs/api/interfaces/placementslottype/)\<[`PlacementSlotProps`](/docs/api/interfaces/placementslotprops/)\<[`ResolvedContent`](/docs/api/type-aliases/resolvedcontent/)\>\>[] *** ### ~~listBySurfaceType()~~ > **listBySurfaceType**(`componentType`): [`PlacementSlotType`](/docs/api/interfaces/placementslottype/)\<[`PlacementSlotProps`](/docs/api/interfaces/placementslotprops/)\<[`ResolvedContent`](/docs/api/type-aliases/resolvedcontent/)\>\>[] :::caution[Deprecated] Use [listByComponentType](/docs/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`](/docs/api/interfaces/placementslottype/)\<[`PlacementSlotProps`](/docs/api/interfaces/placementslotprops/)\<[`ResolvedContent`](/docs/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`](/docs/api/interfaces/placementslotprops/)\<[`ResolvedContent`](/docs/api/type-aliases/resolvedcontent/)\> #### Parameters ##### options [`RegisterPlacementSlotTypeOptions`](/docs/api/type-aliases/registerplacementslottypeoptions/)\<`P`\> #### Returns `void` *** ### resolve() > **resolve**(`output`): [`PlacementSlotType`](/docs/api/interfaces/placementslottype/)\<[`PlacementSlotProps`](/docs/api/interfaces/placementslotprops/)\<[`ResolvedContent`](/docs/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`](/docs/api/interfaces/placementslottype/)\<[`PlacementSlotProps`](/docs/api/interfaces/placementslotprops/)\<[`ResolvedContent`](/docs/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` # 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({ tenantId: 'tenant_abc', apiKey: 'rt_live_xxx', endpoint: 'https://edge.example.com', 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](/docs/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](/docs/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](/docs/api/type-aliases/revturbineplaybookloadstate/). #### Parameters ##### handle `string` ##### context? `RevTurbineEntitlementContext` #### Returns `Promise`\<\{ \}\> *** ### checkoutMetadata() > **checkoutMetadata**(`outputId`): [`RevTurbineCheckoutMetadata`](/docs/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`](/docs/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`](/docs/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`](/docs/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](/docs/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`](/docs/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](/docs/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`](/docs/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](/docs/api/classes/revturbinecustomersdk/#emitsemantic) that constrains the event name to [RevTurbineTriggerEvent](/docs/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"` \| `"feature_gated"` \| `"trial_expired"` \| `"trial_midpoint"` \| `"trial_expiring"` \| `"usage_limit_approaching"` \| `"usage_limit_reached"` \| `"credit_balance_low"` \| `"seat_limit_reached"` \| `"cancel_intent"` \| `"auto_renewal_reminder"` \| `"onboarding_complete"` \| `"invite_teammate_prompt"` \| `"referral_offer"` \| `"plan_upgrade_nudge"` ##### payload? [`RevTurbineTriggerPayload`](/docs/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`](/docs/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`](/docs/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](/docs/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`](/docs/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](/docs/api/type-aliases/revturbinegateresult/). #### Type Parameters ##### T `T` #### Parameters ##### action `string` ##### fn () => `T` \| `Promise`\<`T`\> ##### context? `RevTurbineEntitlementContext` #### Returns `Promise`\<[`RevTurbineGateResult`](/docs/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`](/docs/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](/docs/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`](/docs/api/interfaces/resolvedbranding/) The merged branding and the ladder rung it came from. *** ### getEffectiveUserContext() > **getEffectiveUserContext**(): `Promise`\<`Readonly`\<[`RevTurbineUserContext`](/docs/api/interfaces/revturbineusercontext/)\>\> Return the immutable ephemeral context currently entering local evaluation. #### Returns `Promise`\<`Readonly`\<[`RevTurbineUserContext`](/docs/api/interfaces/revturbineusercontext/)\>\> *** ### getEligibleAddons() > **getEligibleAddons**(): `Promise`\<[`EligibleAddon`](/docs/api/interfaces/eligibleaddon/)[]\> Return the public add-on variations eligible for the active user. #### Returns `Promise`\<[`EligibleAddon`](/docs/api/interfaces/eligibleaddon/)[]\> *** ### getEligiblePlans() > **getEligiblePlans**(): `Promise`\<[`EligiblePlan`](/docs/api/interfaces/eligibleplan/)[]\> Return the public plan variations eligible for the active user. #### Returns `Promise`\<[`EligiblePlan`](/docs/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](/docs/api/classes/revturbinecustomersdk/#getplaybook) — `Playbook` is the canonical name for the artifact (BL-0156). Identical behaviour; removed in `0.12.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](/docs/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](/docs/api/classes/revturbinecustomersdk/#tracktreatmentinteraction), [dismiss](/docs/api/classes/revturbinecustomersdk/#dismiss), [snooze](/docs/api/classes/revturbinecustomersdk/#snooze), [convert](/docs/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](/docs/api/functions/normalizeconfigartifactorthrow/) at their ingestion boundary rather than reading it back out of the SDK. #### Returns \{ \} \| `undefined` *** ### getPolicy() > **getPolicy**(): [`RevTurbinePolicySnapshot`](/docs/api/interfaces/revturbinepolicysnapshot/) Return current SDK policy snapshot. #### Returns [`RevTurbinePolicySnapshot`](/docs/api/interfaces/revturbinepolicysnapshot/) *** ### getRegisteredSlots() > **getRegisteredSlots**(): [`RevTurbineRegisteredSlot`](/docs/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`](/docs/api/interfaces/revturbineregisteredslot/)[] *** ### getTargeting() > **getTargeting**(): [`RevTurbineTargeting`](/docs/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`](/docs/api/interfaces/revturbinetargeting/) *** ### getTelemetryConsent() > **getTelemetryConsent**(): [`TelemetryConsent`](/docs/api/type-aliases/telemetryconsent/) The current behavior-telemetry consent state (plan 144 TASK-8). #### Returns [`TelemetryConsent`](/docs/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`](/docs/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`](/docs/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 — `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. Pre-populates the decision cache, entitlements, trial status, and user context so the client SDK avoids redundant API calls. #### Parameters ##### payload #### Returns `void` #### Example ```ts // In your React component / page hydration: const session = await initRevTurbine({ tenantId, apiKey, endpoint, 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](/docs/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](/docs/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`](/docs/api/type-aliases/identifycontextinput/) #### Parameters ##### userId `string` ##### context? `Exact`\<[`IdentifyContextInput`](/docs/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`](/docs/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](/docs/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`](/docs/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`](/docs/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](/docs/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`](/docs/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`](/docs/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`](/docs/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`](/docs/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](/docs/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](/docs/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](/docs/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` *** ### 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`](/docs/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**(`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](/docs/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). The parameter type does not yet reject other undeclared keys the way [identify](/docs/api/classes/revturbinecustomersdk/#identify) and [update](/docs/api/classes/revturbinecustomersdk/#update) do; that `Exact<>` narrowing is deferred to `0.12.0` because it can break callers that pass a wider object today. #### Parameters ##### userContext [`RevTurbineUserContext`](/docs/api/interfaces/revturbineusercontext/) #### 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](/docs/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](/docs/api/classes/revturbinecustomersdk/#identify) / [setUserContext](/docs/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`](/docs/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](/docs/api/functions/createposthoganalyticsprovider/)). Identity comes from the active user context set via [identify](/docs/api/classes/revturbinecustomersdk/#identify) / [setUserContext](/docs/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](/docs/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`](/docs/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](/docs/api/classes/revturbinecustomersdk/#dismiss)/[snooze](/docs/api/classes/revturbinecustomersdk/#snooze)/[convert](/docs/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](/docs/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](/docs/api/interfaces/revturbineusercontext/) field except the identity handle `id` (see [RevTurbineUpdateInput](/docs/api/type-aliases/revturbineupdateinput/)); to (re)establish identity use [identify](/docs/api/classes/revturbinecustomersdk/#identify). `usage` routes through [updateUsage](/docs/api/classes/revturbinecustomersdk/#updateusage) (absolute balances + usage threshold-crossing events); every other recognized field routes through [setUserContext](/docs/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`](/docs/api/type-aliases/revturbineupdateinput/) #### Parameters ##### patch `Exact`\<[`RevTurbineUpdateInput`](/docs/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](/docs/api/classes/revturbinecustomersdk/#update) alias's `usage` key for the same contract via the general patch verb. #### Parameters ##### balances [`UsageBalances`](/docs/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`\> # RevTurbineServer The RevTurbine server-side client. Holds the server key; mints client sessions ([RevTurbineServer.createClientSession](/docs/api/classes/revturbineserver/#createclientsession)), writes server-only user state ([RevTurbineServer.assignSeatType](/docs/api/classes/revturbineserver/#assignseattype)) and reads the user's server-resolved built-in dimensions ([RevTurbineServer.getBuiltinDimensions](/docs/api/classes/revturbineserver/#getbuiltindimensions)). ## Accessors ### clientSessions #### Get Signature > **get** **clientSessions**(): `object` Client-session minting namespace (plan 157). Ergonomic form of [createClientSession](/docs/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`\> ###### Parameters ###### input `CreateClientSessionInput` ###### Returns `Promise`\<`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](/docs/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` = `{}` 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`\> 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](/docs/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` #### Returns `Promise`\<`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](/docs/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](/docs/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` = `{}` `{ 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`](/docs/api/classes/revturbinecustomersdk/) The underlying SDK instance. Use for advanced/direct operations. *** ### theme > `readonly` **theme**: [`RevTurbineTheme`](/docs/api/interfaces/revturbinetheme/) Resolved theme. ## Methods ### can() > **can**(`handle`, `context?`): `Promise`\<\{ \}\> The entitlement check as a `can` question — alias of [checkEntitlement](/docs/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](/docs/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`](/docs/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](/docs/api/classes/sdksession/#placement) which returns a full controller. #### Parameters ##### slotId `string` ##### options? `Omit`\<[`PlacementControllerOptions`](/docs/api/interfaces/placementcontrolleroptions/), `"placement"` \| `"surfaceSlot"`\> #### Returns `Promise`\<`RevTurbinePlacementDecision` \| `null`\> *** ### getTrialStatus() > **getTrialStatus**(): `Promise`\<\{ \}\> Get the current trial status. #### Returns `Promise`\<\{ \}\> *** ### getUsage() > **getUsage**(): [`RevTurbineUsageSnapshot`](/docs/api/type-aliases/revturbineusagesnapshot/) Get a snapshot of current usage balances. #### Returns [`RevTurbineUsageSnapshot`](/docs/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`](/docs/api/type-aliases/identifycontextinput/), [`IdentifyContextInput`](/docs/api/type-aliases/identifycontextinput/)\> #### Returns `void` *** ### placement() > **placement**(`options`): [`PlacementController`](/docs/api/classes/placementcontroller/) Create a [PlacementController](/docs/api/classes/placementcontroller/) bound to this session's SDK. #### Parameters ##### options [`PlacementControllerOptions`](/docs/api/interfaces/placementcontrolleroptions/) #### Returns [`PlacementController`](/docs/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](/docs/api/classes/revturbinecustomersdk/#resetusercontext). #### Returns `void` *** ### setUserContext() > **setUserContext**(`context`): `void` Merge fields into the current user context. Triggers segment re-evaluation. #### Parameters ##### context [`RevTurbineUserContext`](/docs/api/interfaces/revturbineusercontext/) #### Returns `void` *** ### track() > **track**(`name`, `data?`): `Promise`\<`void`\> Track an event — the advertised alias of [trackEvent](/docs/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`](/docs/api/type-aliases/revturbineupdateinput/) #### Returns `void` *** ### updateUsage() > **updateUsage**(`balances`): `void` Update usage balances (e.g. after a meter event). #### Parameters ##### balances [`UsageBalances`](/docs/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](/docs/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`](/docs/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`](/docs/api/type-aliases/nativeexperimentassignmentunit/) ##### subjectUnit [`NativeExperimentAssignmentUnit`](/docs/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`](/docs/api/type-aliases/nativeexperimentassignmentunit/) Assignment unit requested by the canonical Experiment version. *** ### subjectUnit > `readonly` **subjectUnit**: [`NativeExperimentAssignmentUnit`](/docs/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`](/docs/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`](/docs/api/interfaces/canonicalexperimentallocation/) ## Returns [`BasicBucketerExperiment`](/docs/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`](/docs/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`](/docs/api/type-aliases/resolvedcontent/) = [`ResolvedContent`](/docs/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`](/docs/api/interfaces/revturbinetheme/)\> The base palette for a resolved scheme. ## Parameters ### scheme `"light"` \| `"dark"` `'light'` or `'dark'`. Resolve `'system'` before calling. ## Returns `Readonly`\<[`RevTurbineTheme`](/docs/api/interfaces/revturbinetheme/)\> The matching base theme. # bridgeUiPathResolversIntoRegistry > **bridgeUiPathResolversIntoRegistry**(`resolvers`, `registry?`): () => `void` Bridge an SDK-init `uiPathResolvers` map into a [CtaResolverRegistry](/docs/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](/docs/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`](/docs/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`](/docs/api/interfaces/basicbucketerexperiment/) ## Returns `string` \| `undefined` # buildControlPlaneEvent > **buildControlPlaneEvent**(`eventType`, `payload?`): [`ControlPlaneEmitInput`](/docs/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](/docs/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`](/docs/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`](/docs/api/type-aliases/buttoncomponentprops/) ## Returns `any` # categorizeActionError > **categorizeActionError**(`err`): [`ActionErrorCategory`](/docs/api/type-aliases/actionerrorcategory/) Bucket an unknown thrown value into a [ActionErrorCategory](/docs/api/type-aliases/actionerrorcategory/) without surfacing its message (plan 144 TASK-14 — non-sensitive categories only). ## Parameters ### err `unknown` ## Returns [`ActionErrorCategory`](/docs/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`](/docs/api/interfaces/experimentassignmentfactdeclaration/)\> Harvest valid assignment-fact declarations from providers exposing the [ExperimentAssignmentFactMetadataCarrier](/docs/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`](/docs/api/interfaces/experimentassignmentfactdeclaration/)[] ## Returns `ReadonlyMap`\<`string`, [`ExperimentAssignmentFactDeclaration`](/docs/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`](/docs/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`](/docs/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`](/docs/api/interfaces/compositeexperimentprovideroptions/) = `{}` ## Returns `ExperimentAssignmentProvider` # createCustomEndpointRuntimeConfig > **createCustomEndpointRuntimeConfig**(`options`): `RevTurbineInitOptions` Build a custom-endpoint SDK config. ## Parameters ### options [`RevTurbineInitBaseOptions`](/docs/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`](/docs/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`](/docs/api/type-aliases/revturbineinitbaseoptions/) & `object` ### Returns `RevTurbineInitOptions` ## Call Signature > **createLocalRuntimeConfig**(`options`): `RevTurbineInitOptions` Build a local-only SDK config. ### Parameters #### options [`RevTurbineInitBaseOptions`](/docs/api/type-aliases/revturbineinitbaseoptions/) & `object` ### Returns `RevTurbineInitOptions` # createNativeExperimentAssignmentProvider > **createNativeExperimentAssignmentProvider**(`options`): `ExperimentAssignmentProvider` & [`ExperimentAssignmentFactMetadataCarrier`](/docs/api/interfaces/experimentassignmentfactmetadatacarrier/) Build the native assignment provider whose allocation authority is the canonical Experiment version rather than duplicated SDK configuration. ## Parameters ### options [`NativeExperimentAssignmentOptions`](/docs/api/interfaces/nativeexperimentassignmentoptions/) ## Returns `ExperimentAssignmentProvider` & [`ExperimentAssignmentFactMetadataCarrier`](/docs/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](/docs/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](/docs/api/functions/createanalyticsprovider/), a throw from `posthog.capture` is swallowed — a PostHog failure never crashes the SDK event pipeline. ## Parameters ### options [`PostHogAnalyticsProviderOptions`](/docs/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](/docs/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`](/docs/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`](/docs/api/interfaces/revturbinesemanticevent/) Create a typed semantic event payload. ## Parameters ### eventType `string` ### payload [`SdkEventProperties`](/docs/api/type-aliases/sdkeventproperties/) ### options? `RevTurbineEventOptions` ## Returns [`RevTurbineSemanticEvent`](/docs/api/interfaces/revturbinesemanticevent/) # createServerRuntimeConfig > **createServerRuntimeConfig**(`options`): `RevTurbineInitOptions` Build a server-backed SDK config (default runtime mode). ## Parameters ### options [`RevTurbineInitBaseOptions`](/docs/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`](/docs/api/interfaces/revturbineslotplacementrequestoptions/) = `{}` ## Returns `RevTurbinePlacementRequestConfig` # createStrictLocalRuntimeConfig > **createStrictLocalRuntimeConfig**\<`TUiPaths`\>(`options`): `RevTurbineInitOptions` Build a strict local-only SDK config. Unlike [createLocalRuntimeConfig](/docs/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`](/docs/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`](/docs/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`](/docs/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`](/docs/api/type-aliases/revturbinerequireduipathresolvers/)\<`TUiPaths`\> & [`RevTurbineUiPathResolverMap`](/docs/api/type-aliases/revturbineuipathresolvermap/) ## Returns [`RevTurbineUiPathResolverMap`](/docs/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`](/docs/api/interfaces/placementuipath/) ### context [`CtaResolverContext`](/docs/api/interfaces/ctaresolvercontext/) ### resolvers [`CtaResolverRegistry`](/docs/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`](/docs/api/interfaces/placementslotprops/) ## Returns `any` # EngagementArea > **EngagementArea**(`__namedParameters`): `ReactElement` A qualified engagement region (plan 144 TASK-13 / REQ-21). It: - establishes a [TelemetryScope](/docs/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`](/docs/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](/docs/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`](/docs/api/classes/ctaresolverregistry/) Get or create the default global CTA resolver registry. The [PlacementRenderer](/docs/api/functions/placementrenderer/) uses this when no `ctaResolvers` prop is supplied. ## Returns [`CtaResolverRegistry`](/docs/api/classes/ctaresolverregistry/) # getDefaultRegistry > **getDefaultRegistry**(): [`PlacementTypeRegistry`](/docs/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](/docs/api/classes/placementtyperegistry/#register)). ## Returns [`PlacementTypeRegistry`](/docs/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`](/docs/api/interfaces/initfailurediagnosticprops/) ## Returns `any` # initRevTurbine > **initRevTurbine**\<`TUser`\>(`options`): `Promise`\<[`SdkSession`](/docs/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](/docs/api/classes/sdksession/) exposes the full imperative API: user context management, placement controllers, entitlement gates, and event tracking. ## Type Parameters ### TUser `TUser` *extends* [`RevTurbineUserContext`](/docs/api/interfaces/revturbineusercontext/) = [`RevTurbineUserContext`](/docs/api/interfaces/revturbineusercontext/) ## Parameters ### options [`SdkSessionOptions`](/docs/api/type-aliases/sdksessionoptions/) & `object` ## Returns `Promise`\<[`SdkSession`](/docs/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`](/docs/api/interfaces/revturbineinitstatus/) Build the failure status for a thrown init error. ## Parameters ### phase [`RevTurbineInitPhase`](/docs/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`](/docs/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`](/docs/api/type-aliases/resolvedcontent/) = [`ResolvedContent`](/docs/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`](/docs/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`](/docs/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](/docs/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`](/docs/api/interfaces/revturbinetheme/)\> # mergeTheme > **mergeTheme**(`input?`, `base?`): [`RevTurbineTheme`](/docs/api/interfaces/revturbinetheme/) Deep-merge a partial theme input with a base theme, producing a complete [RevTurbineTheme](/docs/api/interfaces/revturbinetheme/). ## Parameters ### input? [`RevTurbineThemeInput`](/docs/api/type-aliases/revturbinethemeinput/) \| `null` Partial tokens. Every omitted token falls back to `base`. ### base? `Readonly`\<[`RevTurbineTheme`](/docs/api/interfaces/revturbinetheme/)\> = `DEFAULT_THEME` The palette to merge over. Defaults to [DEFAULT\_THEME](/docs/api/variables/default_theme/); pass [DARK\_THEME](/docs/api/variables/dark_theme/) (or use [baseThemeForScheme](/docs/api/functions/basethemeforscheme/)) to resolve a partial brand theme against the dark palette instead. ## Returns [`RevTurbineTheme`](/docs/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`](/docs/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`](/docs/api/type-aliases/resolvedcontent/) = [`ResolvedContent`](/docs/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`](/docs/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](/docs/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`](/docs/api/interfaces/placementuipath/) Parse a `cta_path` (or legacy `ui_path`) record from a placement output into a typed [PlacementUiPath](/docs/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](/docs/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`](/docs/api/interfaces/placementuipath/) # Placement > **Placement**(`__namedParameters`): `any` Render-prop component for placement integration. An alternative to [usePlacement](/docs/api/functions/useplacement/) for class components or when render-prop composition is preferred. ## Parameters ### \_\_namedParameters [`PlacementProps`](/docs/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`](/docs/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`](/docs/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`](/docs/api/type-aliases/resolvedcontent/) = [`ResolvedContent`](/docs/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`](/docs/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`](/docs/api/classes/ctaresolverregistry/) = `...` ## Returns () => `void` # registerCtaResolver > **registerCtaResolver**(`type`, `resolver`): `void` Register a [CtaResolver](/docs/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`](/docs/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`](/docs/api/type-aliases/serveractionmap/) ### callbacks `ServerActionResolverCallbacks` ### registry? [`CtaResolverRegistry`](/docs/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`](/docs/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`](/docs/api/interfaces/brandingresolutioninput/) = `{}` The available branding sources; see [BrandingResolutionInput](/docs/api/interfaces/brandingresolutioninput/). ## Returns [`ResolvedBranding`](/docs/api/interfaces/resolvedbranding/) The merged branding and the rung it came from. # resolveContent > **resolveContent**(`content`, `context`): [`ResolvedContent`](/docs/api/type-aliases/resolvedcontent/) Resolve all personalization tokens in a content object. ## Parameters ### content `Record`\<`string`, `unknown`\> ### context [`PersonalizationContext`](/docs/api/interfaces/personalizationcontext/) ## Returns [`ResolvedContent`](/docs/api/type-aliases/resolvedcontent/) # resolveLocalPlaybook > **resolveLocalPlaybook**(`localRuntime?`): [`ConfigArtifact`](/docs/api/type-aliases/configartifact/) \| [`UnvalidatedConfigArtifact`](/docs/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](/docs/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`](/docs/api/type-aliases/configartifact/) \| [`UnvalidatedConfigArtifact`](/docs/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`](/docs/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`](/docs/api/interfaces/revturbineusercontext/) = [`RevTurbineUserContext`](/docs/api/interfaces/revturbineusercontext/) ### TOptions `TOptions` *extends* [`RevTurbineInitInputOptions`](/docs/api/type-aliases/revturbineinitinputoptions/) = [`RevTurbineInitInputOptions`](/docs/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`](/docs/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](/docs/api/interfaces/revturbinetheme/) to all child SDK components. Typically rendered internally by [RevTurbineProvider](/docs/api/functions/revturbineprovider/) — consumers don't need to add this manually. The theme is merged over [DEFAULT\_THEME](/docs/api/variables/default_theme/) here, so [useRevTurbineTheme](/docs/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`](/docs/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](/docs/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`](/docs/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`](/docs/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`](/docs/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`](/docs/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](/docs/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](/docs/api/functions/useentitlement/) directly. ## Parameters ### handle `string` ### options? `Omit`\<`UseEntitlementOptions`, `"handle"`\> ## Returns [`UseCanResult`](/docs/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`](/docs/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`](/docs/api/interfaces/trackoptions/) ## Returns [`GatedAction`](/docs/api/interfaces/gatedaction/)\<`A`, `R`\> # usePlacement > **usePlacement**(`__namedParameters`): [`UsePlacementResult`](/docs/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`](/docs/api/type-aliases/useplacementoptions/) ## Returns [`UsePlacementResult`](/docs/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`](/docs/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`](/docs/api/interfaces/useplacementpersonalizationoptions/) ## Returns [`PersonalizationContext`](/docs/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](/docs/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`](/docs/api/type-aliases/revturbinecolorscheme/) The requested scheme. `'system'` follows the OS. ## Returns `"light"` \| `"dark"` `'light'` or `'dark'`. # useRevTurbineTheme > **useRevTurbineTheme**(): [`RevTurbineTheme`](/docs/api/interfaces/revturbinetheme/) Access the current SDK theme tokens from any component inside a [RevTurbineProvider](/docs/api/functions/revturbineprovider/) tree. ## Returns [`RevTurbineTheme`](/docs/api/interfaces/revturbinetheme/) # useRevTurbineThemeProviderPresent > **useRevTurbineThemeProviderPresent**(): `boolean` Whether a [RevTurbineThemeProvider](/docs/api/functions/revturbinethemeprovider/) is already mounted above the caller. Used by [RevTurbineProvider](/docs/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`](/docs/api/interfaces/userprofileprops/) ## Returns `any` # useSlotRoute > **useSlotRoute**(): `string` \| `null` The route a slot rendered here would report: the nearest [RevTurbineRoute](/docs/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](/docs/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`](/docs/api/type-aliases/sdkeventproperties/) ### options? [`TrackOptions`](/docs/api/interfaces/trackoptions/) ## Returns `object` ### onClick > **onClick**: `MouseEventHandler` ## Example ```tsx const t = useTelemetryProps('cta_clicked', { plan: 'pro' }); ``` # useTelemetryScope > **useTelemetryScope**(): [`TelemetryScopeValue`](/docs/api/interfaces/telemetryscopevalue/) The current merged telemetry scope (plan 144 TASK-12). Empty when outside any [TelemetryScope](/docs/api/functions/telemetryscope/). ## Returns [`TelemetryScopeValue`](/docs/api/interfaces/telemetryscopevalue/) # useTrack > **useTrack**(): [`TrackFn`](/docs/api/type-aliases/trackfn/) Returns a `track(name, data?, options?)` function bound to the enclosing [TelemetryScope](/docs/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`](/docs/api/type-aliases/trackfn/) # useTrackedAction > **useTrackedAction**\<`A`, `R`\>(`name`, `fn`, `options?`): [`TrackedAction`](/docs/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](/docs/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`](/docs/api/interfaces/trackoptions/) ## Returns [`TrackedAction`](/docs/api/interfaces/trackedaction/)\<`A`, `R`\> # AnalyticsProviderOptions Configuration for [createAnalyticsProvider](/docs/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`](/docs/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`](/docs/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. # 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](/docs/api/functions/resolvebranding/). Every field is optional; with none supplied the ladder resolves to [DEFAULT\_BRANDING](/docs/api/variables/default_branding/). ## Properties ### apiBranding? > `optional` **apiBranding?**: `object` Rung 3 — branding fetched from the Branding API / workspace settings. *** ### 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](/docs/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. # 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](/docs/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](/docs/api/functions/buildcontrolplaneevent/). `eventName` is the control-plane `event_type`; `properties` is the payload with the source classification stamped under [CONTROL\_PLANE\_SOURCE\_KEY](/docs/api/variables/control_plane_source_key/). # CtaResolverContext Context handed to a [CtaResolver](/docs/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](/docs/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`](/docs/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`](/docs/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`](/docs/api/interfaces/experimentassignmentfactdeclaration/)[] Declarations describing the canonical versions this provider allocates. # FeatureGateTriggerPayload Payload for feature-gate trigger events. # GatedAction State + runner returned by [useGatedAction](/docs/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`](/docs/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`](/docs/api/type-aliases/revturbinegateresult/)\<`R`\>\> Run the gated action. Returns the [RevTurbineGateResult](/docs/api/type-aliases/revturbinegateresult/): `ran: true` with the result, or `ran: false` with the entitlement. #### Parameters ##### args ...`A` #### Returns `Promise`\<[`RevTurbineGateResult`](/docs/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`](/docs/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`](/docs/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`](/docs/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](/docs/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`](/docs/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](/docs/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`](/docs/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](/docs/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](/docs/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](/docs/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`](/docs/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`](/docs/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`](/docs/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`](/docs/api/type-aliases/resolvedcontent/) = [`ResolvedContent`](/docs/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`](/docs/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`](/docs/api/interfaces/placementslotprops/) = [`PlacementSlotProps`](/docs/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](/docs/api/functions/parseuipath/) lifts the known fields (`url`, `plan_handle`, …) onto this object and collects every remaining key here so a custom [CtaResolver](/docs/api/type-aliases/ctaresolver/) can read tenant-specific parameters. Omitted when the action carries no extra params. # PostHogAnalyticsProviderOptions Configuration for [createPostHogAnalyticsProvider](/docs/api/functions/createposthoganalyticsprovider/). ## Extended by - [`PostHogIntegrationOptions`](/docs/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`](/docs/api/interfaces/posthoglike/) The PostHog client instance (`posthog-js` or `posthog-node`). *** ### transform? > `optional` **transform?**: [`AnalyticsEventTransformer`](/docs/api/type-aliases/analyticseventtransformer/) Optional transform applied before capture (rename / enrich / drop). # PostHogIntegrationOptions Configuration for [createPostHogIntegration](/docs/api/functions/createposthogintegration/). ## Extends - [`PostHogAnalyticsProviderOptions`](/docs/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`](/docs/api/interfaces/posthoganalyticsprovideroptions/).[`filter`](/docs/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`](/docs/api/interfaces/posthoganalyticsprovideroptions/).[`name`](/docs/api/interfaces/posthoganalyticsprovideroptions/#name) *** ### posthog > **posthog**: [`PostHogLike`](/docs/api/interfaces/posthoglike/) The PostHog client instance (`posthog-js` or `posthog-node`). #### Inherited from [`PostHogAnalyticsProviderOptions`](/docs/api/interfaces/posthoganalyticsprovideroptions/).[`posthog`](/docs/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`](/docs/api/type-aliases/analyticseventtransformer/) Optional transform applied before capture (rename / enrich / drop). #### Inherited from [`PostHogAnalyticsProviderOptions`](/docs/api/interfaces/posthoganalyticsprovideroptions/).[`transform`](/docs/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](/docs/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](/docs/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](/docs/api/variables/default_branding/). *** ### source > `readonly` **source**: [`BrandingSource`](/docs/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](/docs/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`](/docs/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`](/docs/api/type-aliases/configartifact/) \| `undefined` :::caution[Deprecated] Renamed to [RevTurbineConfigProvider.getPlaybook](/docs/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.12.0`. ::: #### Returns [`ConfigArtifact`](/docs/api/type-aliases/configartifact/) \| `undefined` *** ### getPlaybook()? > `optional` **getPlaybook**(): [`ConfigArtifact`](/docs/api/type-aliases/configartifact/) \| `undefined` Return the latest available Playbook snapshot. #### Returns [`ConfigArtifact`](/docs/api/type-aliases/configartifact/) \| `undefined` *** ### refresh()? > `optional` **refresh**(): `Promise`\<[`ConfigArtifact`](/docs/api/type-aliases/configartifact/) \| `undefined`\> Optionally refresh the snapshot (for example, from a REST API). #### Returns `Promise`\<[`ConfigArtifact`](/docs/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`](/docs/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`](/docs/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. # RevTurbinePolicySnapshot Snapshot of policy-related SDK runtime settings and loaded config metadata. ## Properties ### ~~exportedConfigVersion?~~ > `optional` **exportedConfigVersion?**: `string` :::caution[Deprecated] Renamed to [RevTurbinePolicySnapshot.playbookVersion](/docs/api/interfaces/revturbinepolicysnapshot/#playbookversion) (BL-0156). Still emitted with the identical value for one minor; removed in `0.12.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](/docs/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`](/docs/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 tokens → 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](/docs/api/classes/revturbinecustomersdk/#diagnoseslotinventory). ## Properties ### authored > **authored**: [`RevTurbineAuthoredSlotRef`](/docs/api/interfaces/revturbineauthoredslotref/)[] Every slot-targeted placement in the Playbook. *** ### authoredButUnmounted > **authoredButUnmounted**: [`RevTurbineAuthoredSlotRef`](/docs/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`](/docs/api/interfaces/revturbineregisteredslot/)[] Every slot mounted by this app right now. *** ### mountedButUnauthored > **mountedButUnauthored**: [`RevTurbineRegisteredSlot`](/docs/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`](/docs/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`](/docs/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](/docs/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`](/docs/api/interfaces/revturbinethemecolors/) Color tokens. *** ### id? > `optional` **id?**: `string` Theme identifier. *** ### name? > `optional` **name?**: `string` Human-readable theme name. *** ### shadows > **shadows**: [`RevTurbineThemeShadows`](/docs/api/interfaces/revturbinethemeshadows/) Shadow tokens. *** ### shape > **shape**: [`RevTurbineThemeShape`](/docs/api/interfaces/revturbinethemeshape/) Shape tokens. *** ### typography > **typography**: [`RevTurbineThemeTypography`](/docs/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](/docs/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()](/docs/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()](/docs/api/classes/revturbinecustomersdk/#getusercontext). A plain-JS caller that passes any of these keys has them dropped and reported at runtime. ## Extends - `Omit`\<[`UserContextInput`](/docs/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`](/docs/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`](/docs/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`](/docs/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`](/docs/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` # TelemetryScopeProps Props for [TelemetryScope](/docs/api/functions/telemetryscope/). ## Extends - [`TelemetryScopeValue`](/docs/api/interfaces/telemetryscopevalue/) ## Properties ### action? > `optional` **action?**: `string` Default action label for events in this scope. #### Inherited from [`TelemetryScopeValue`](/docs/api/interfaces/telemetryscopevalue/).[`action`](/docs/api/interfaces/telemetryscopevalue/#action) *** ### area? > `optional` **area?**: `string` Logical product area (e.g. `'billing'`, `'editor'`). #### Inherited from [`TelemetryScopeValue`](/docs/api/interfaces/telemetryscopevalue/).[`area`](/docs/api/interfaces/telemetryscopevalue/#area) *** ### purpose? > `optional` **purpose?**: [`TrackPurpose`](/docs/api/type-aliases/trackpurpose/) Advisory [TrackPurpose](/docs/api/type-aliases/trackpurpose/). #### Inherited from [`TelemetryScopeValue`](/docs/api/interfaces/telemetryscopevalue/).[`purpose`](/docs/api/interfaces/telemetryscopevalue/#purpose) # TelemetryScopeValue Ambient telemetry context established by [TelemetryScope](/docs/api/functions/telemetryscope/) and merged into every event emitted by [useTrack](/docs/api/functions/usetrack/) beneath it. ## Extended by - [`TelemetryScopeProps`](/docs/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`](/docs/api/type-aliases/trackpurpose/) Advisory [TrackPurpose](/docs/api/type-aliases/trackpurpose/). # TrackedAction State + runner returned by [useTrackedAction](/docs/api/functions/usetrackedaction/). ## Type Parameters ### A `A` *extends* `unknown`[] ### R `R` ## Properties ### error > **error**: [`ActionErrorCategory`](/docs/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](/docs/api/functions/trackonview/). ## Properties ### as? > `optional` **as?**: `string` \| `number` \| `symbol` Host element tag. Default `'div'`. *** ### data? > `optional` **data?**: [`SdkEventProperties`](/docs/api/type-aliases/sdkeventproperties/) Event data (reserved names are dropped by [useTrack](/docs/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`](/docs/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](/docs/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`](/docs/api/type-aliases/trackpurpose/) Advisory [TrackPurpose](/docs/api/type-aliases/trackpurpose/); overrides the scope purpose. # TrackProps Props for [Track](/docs/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`](/docs/api/type-aliases/sdkeventproperties/) Event data (reserved names are dropped by [useTrack](/docs/api/functions/usetrack/)). *** ### event > **event**: `string` Event name to emit on click. *** ### options? > `optional` **options?**: [`TrackOptions`](/docs/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](/docs/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`](/docs/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](/docs/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`](/docs/api/interfaces/revturbinetargeting/) Targeting/user context snapshot to render. *** ### title? > `optional` **title?**: `string` Optional heading override. # @revt-eng/sdk ## Classes - [BrowserStorage](/docs/api/classes/browserstorage/) - [CtaResolverRegistry](/docs/api/classes/ctaresolverregistry/) - [PlacementController](/docs/api/classes/placementcontroller/) - [PlacementTypeRegistry](/docs/api/classes/placementtyperegistry/) - [RevTurbineApiError](/docs/api/classes/revturbineapierror/) - [RevTurbineCustomerSdk](/docs/api/classes/revturbinecustomersdk/) - [RevTurbineServer](/docs/api/classes/revturbineserver/) - [SdkSession](/docs/api/classes/sdksession/) - [ServerUserContextProvider](/docs/api/classes/serverusercontextprovider/) - [UnsupportedExperimentAssignmentUnitError](/docs/api/classes/unsupportedexperimentassignmentuniterror/) ## Interfaces - [AnalyticsProviderOptions](/docs/api/interfaces/analyticsprovideroptions/) - [BasicBucketerExperiment](/docs/api/interfaces/basicbucketerexperiment/) - [BrandingResolutionInput](/docs/api/interfaces/brandingresolutioninput/) - [CanonicalExperimentAllocation](/docs/api/interfaces/canonicalexperimentallocation/) - [CompositeExperimentProviderOptions](/docs/api/interfaces/compositeexperimentprovideroptions/) - [ConfigTargetDefaults](/docs/api/interfaces/configtargetdefaults/) - [ControlPlaneEmitInput](/docs/api/interfaces/controlplaneemitinput/) - [CtaResolverContext](/docs/api/interfaces/ctaresolvercontext/) - [EligibleAddon](/docs/api/interfaces/eligibleaddon/) - [EligiblePlan](/docs/api/interfaces/eligibleplan/) - [EligiblePrice](/docs/api/interfaces/eligibleprice/) - [EngagementAreaProps](/docs/api/interfaces/engagementareaprops/) - [EntitlementGateOptions](/docs/api/interfaces/entitlementgateoptions/) - [EntitlementGateState](/docs/api/interfaces/entitlementgatestate/) - [ExperimentAssignmentFactDeclaration](/docs/api/interfaces/experimentassignmentfactdeclaration/) - [ExperimentAssignmentFactMetadataCarrier](/docs/api/interfaces/experimentassignmentfactmetadatacarrier/) - [FeatureGateTriggerPayload](/docs/api/interfaces/featuregatetriggerpayload/) - [GatedAction](/docs/api/interfaces/gatedaction/) - [InitFailureDiagnosticProps](/docs/api/interfaces/initfailurediagnosticprops/) - [NativeExperimentAssignmentOptions](/docs/api/interfaces/nativeexperimentassignmentoptions/) - [PaymentTriggerPayload](/docs/api/interfaces/paymenttriggerpayload/) - [PersonalizationContext](/docs/api/interfaces/personalizationcontext/) - [PlacementContentFields](/docs/api/interfaces/placementcontentfields/) - [PlacementControllerOptions](/docs/api/interfaces/placementcontrolleroptions/) - [PlacementControllerState](/docs/api/interfaces/placementcontrollerstate/) - [PlacementCustomCode](/docs/api/interfaces/placementcustomcode/) - [PlacementDecisionInspectorProps](/docs/api/interfaces/placementdecisioninspectorprops/) - [PlacementPreviewConfig](/docs/api/interfaces/placementpreviewconfig/) - [PlacementSlotProps](/docs/api/interfaces/placementslotprops/) - [PlacementSlotType](/docs/api/interfaces/placementslottype/) - [PlacementUiPath](/docs/api/interfaces/placementuipath/) - [PostHogAnalyticsProviderOptions](/docs/api/interfaces/posthoganalyticsprovideroptions/) - [PostHogIntegrationOptions](/docs/api/interfaces/posthogintegrationoptions/) - [PostHogLike](/docs/api/interfaces/posthoglike/) - [ResolvedBranding](/docs/api/interfaces/resolvedbranding/) - [RevTurbineApiPaths](/docs/api/interfaces/revturbineapipaths/) - [RevTurbineAuthoredSlotRef](/docs/api/interfaces/revturbineauthoredslotref/) - [RevTurbineChainedPlacementRequestOptions](/docs/api/interfaces/revturbinechainedplacementrequestoptions/) - [RevTurbineCheckoutMetadata](/docs/api/interfaces/revturbinecheckoutmetadata/) - [RevTurbineCheckoutMetadataBag](/docs/api/interfaces/revturbinecheckoutmetadatabag/) - [RevTurbineConfigProvider](/docs/api/interfaces/revturbineconfigprovider/) - [RevTurbineContextPolicy](/docs/api/interfaces/revturbinecontextpolicy/) - [RevTurbineEndpointOverrides](/docs/api/interfaces/revturbineendpointoverrides/) - [RevTurbineEntitlementPlacementRequestOptions](/docs/api/interfaces/revturbineentitlementplacementrequestoptions/) - [RevTurbineEntitlementRuleEvaluation](/docs/api/interfaces/revturbineentitlementruleevaluation/) - [RevTurbineEventBatchingOptions](/docs/api/interfaces/revturbineeventbatchingoptions/) - [RevTurbineInitStatus](/docs/api/interfaces/revturbineinitstatus/) - [RevTurbinePageContext](/docs/api/interfaces/revturbinepagecontext/) - [RevTurbinePlacementBehaviorFlags](/docs/api/interfaces/revturbineplacementbehaviorflags/) - [~~RevTurbinePlacementConfig~~](/docs/api/interfaces/revturbineplacementconfig/) - [RevTurbinePlacementDecisionExplanation](/docs/api/interfaces/revturbineplacementdecisionexplanation/) - [RevTurbinePlacementPayloadEvaluation](/docs/api/interfaces/revturbineplacementpayloadevaluation/) - [RevTurbinePlacementRuleEvaluation](/docs/api/interfaces/revturbineplacementruleevaluation/) - [RevTurbinePlacementTypeEntity](/docs/api/interfaces/revturbineplacementtypeentity/) - [RevTurbinePolicySnapshot](/docs/api/interfaces/revturbinepolicysnapshot/) - [RevTurbineRegisteredSlot](/docs/api/interfaces/revturbineregisteredslot/) - [RevTurbineRouteProps](/docs/api/interfaces/revturbinerouteprops/) - [RevTurbineSegmentEvaluation](/docs/api/interfaces/revturbinesegmentevaluation/) - [RevTurbineSemanticEvent](/docs/api/interfaces/revturbinesemanticevent/) - [RevTurbineServerOptions](/docs/api/interfaces/revturbineserveroptions/) - [RevTurbineSlotInventoryDiagnosis](/docs/api/interfaces/revturbineslotinventorydiagnosis/) - [RevTurbineSlotPlacementRequestOptions](/docs/api/interfaces/revturbineslotplacementrequestoptions/) - [RevTurbineSurfaceSlotConfig](/docs/api/interfaces/revturbinesurfaceslotconfig/) - [RevTurbineTargeting](/docs/api/interfaces/revturbinetargeting/) - [RevTurbineTelemetryOptions](/docs/api/interfaces/revturbinetelemetryoptions/) - [RevTurbineTheme](/docs/api/interfaces/revturbinetheme/) - [RevTurbineThemeColors](/docs/api/interfaces/revturbinethemecolors/) - [RevTurbineThemeShadows](/docs/api/interfaces/revturbinethemeshadows/) - [RevTurbineThemeShape](/docs/api/interfaces/revturbinethemeshape/) - [RevTurbineThemeTypography](/docs/api/interfaces/revturbinethemetypography/) - [RevTurbineTreatmentInteractionOptions](/docs/api/interfaces/revturbinetreatmentinteractionoptions/) - [RevTurbineUsageSnapshotEntry](/docs/api/interfaces/revturbineusagesnapshotentry/) - [RevTurbineUserContext](/docs/api/interfaces/revturbineusercontext/) - [ServerActionContext](/docs/api/interfaces/serveractioncontext/) - [ServerActionResult](/docs/api/interfaces/serveractionresult/) - [ServerEvaluationRequest](/docs/api/interfaces/serverevaluationrequest/) - [ServerPlacementRequest](/docs/api/interfaces/serverplacementrequest/) - [ServerUserContextSnapshot](/docs/api/interfaces/serverusercontextsnapshot/) - [TelemetryScopeProps](/docs/api/interfaces/telemetryscopeprops/) - [TelemetryScopeValue](/docs/api/interfaces/telemetryscopevalue/) - [TrackedAction](/docs/api/interfaces/trackedaction/) - [TrackOnViewProps](/docs/api/interfaces/trackonviewprops/) - [TrackOptions](/docs/api/interfaces/trackoptions/) - [TrackProps](/docs/api/interfaces/trackprops/) - [TrialTriggerPayload](/docs/api/interfaces/trialtriggerpayload/) - [UsageTriggerPayload](/docs/api/interfaces/usagetriggerpayload/) - [UseCanResult](/docs/api/interfaces/usecanresult/) - [UsePlacementPersonalizationOptions](/docs/api/interfaces/useplacementpersonalizationoptions/) - [UserProfileProps](/docs/api/interfaces/userprofileprops/) ## Type Aliases - [ActionErrorCategory](/docs/api/type-aliases/actionerrorcategory/) - [AnalyticsEventHandler](/docs/api/type-aliases/analyticseventhandler/) - [AnalyticsEventProperties](/docs/api/type-aliases/analyticseventproperties/) - [AnalyticsEventTransformer](/docs/api/type-aliases/analyticseventtransformer/) - [AssignmentFactUnit](/docs/api/type-aliases/assignmentfactunit/) - [BannerComponentProps](/docs/api/type-aliases/bannercomponentprops/) - [BrandingSource](/docs/api/type-aliases/brandingsource/) - [BrowserRuntimeOptions](/docs/api/type-aliases/browserruntimeoptions/) - [ButtonComponentProps](/docs/api/type-aliases/buttoncomponentprops/) - [ChangeListener](/docs/api/type-aliases/changelistener/) - [CliComponentProps](/docs/api/type-aliases/clicomponentprops/) - [ConfigArtifact](/docs/api/type-aliases/configartifact/) - [CreditBalanceComponentProps](/docs/api/type-aliases/creditbalancecomponentprops/) - [CtaResolver](/docs/api/type-aliases/ctaresolver/) - [EmittablePlatformEventName](/docs/api/type-aliases/emittableplatformeventname/) - [EntitlementStatus](/docs/api/type-aliases/entitlementstatus/) - [ExactInitOptions](/docs/api/type-aliases/exactinitoptions/) - [~~ExportedConfigProvider~~](/docs/api/type-aliases/exportedconfigprovider/) - [FullPageComponentProps](/docs/api/type-aliases/fullpagecomponentprops/) - [IdentifyContextInput](/docs/api/type-aliases/identifycontextinput/) - [InlineEmbedComponentProps](/docs/api/type-aliases/inlineembedcomponentprops/) - [InPageComponentProps](/docs/api/type-aliases/inpagecomponentprops/) - [~~LegacyConfigTargetDefaults~~](/docs/api/type-aliases/legacyconfigtargetdefaults/) - [MessageTriggerType](/docs/api/type-aliases/messagetriggertype/) - [ModalComponentProps](/docs/api/type-aliases/modalcomponentprops/) - [ModalLayout](/docs/api/type-aliases/modallayout/) - [NativeExperimentAssignmentUnit](/docs/api/type-aliases/nativeexperimentassignmentunit/) - [PlacementPromotion](/docs/api/type-aliases/placementpromotion/) - [PlacementProps](/docs/api/type-aliases/placementprops/) - [PlacementUiPathActionType](/docs/api/type-aliases/placementuipathactiontype/) - [PlaybookProvider](/docs/api/type-aliases/playbookprovider/) - [QuotaMeterComponentProps](/docs/api/type-aliases/quotametercomponentprops/) - [RecognizedIdentifyKey](/docs/api/type-aliases/recognizedidentifykey/) - [RecognizedUpdateKey](/docs/api/type-aliases/recognizedupdatekey/) - [RegisterPlacementSlotTypeOptions](/docs/api/type-aliases/registerplacementslottypeoptions/) - [ResolvedContent](/docs/api/type-aliases/resolvedcontent/) - [RevTurbineClientSessionProvider](/docs/api/type-aliases/revturbineclientsessionprovider/) - [RevTurbineColorScheme](/docs/api/type-aliases/revturbinecolorscheme/) - [RevTurbineConfigProviderInput](/docs/api/type-aliases/revturbineconfigproviderinput/) - [RevTurbineGateResult](/docs/api/type-aliases/revturbinegateresult/) - [RevTurbineImpressionMetadata](/docs/api/type-aliases/revturbineimpressionmetadata/) - [RevTurbineInitBaseOptions](/docs/api/type-aliases/revturbineinitbaseoptions/) - [RevTurbineInitInputOptions](/docs/api/type-aliases/revturbineinitinputoptions/) - [RevTurbineInitOptionsStrict](/docs/api/type-aliases/revturbineinitoptionsstrict/) - [RevTurbineInitPhase](/docs/api/type-aliases/revturbineinitphase/) - [RevTurbineLocalOnlyMinimalInitOptions](/docs/api/type-aliases/revturbinelocalonlyminimalinitoptions/) - [RevTurbinePlaybookLoadState](/docs/api/type-aliases/revturbineplaybookloadstate/) - [RevTurbineRequiredUiPathResolvers](/docs/api/type-aliases/revturbinerequireduipathresolvers/) - [RevTurbineRuntimeMode](/docs/api/type-aliases/revturbineruntimemode/) - [RevTurbineSdkMode](/docs/api/type-aliases/revturbinesdkmode/) - [RevTurbineSegmentPredicateEvaluation](/docs/api/type-aliases/revturbinesegmentpredicateevaluation/) - [RevTurbineThemeInput](/docs/api/type-aliases/revturbinethemeinput/) - [RevTurbineTreatmentInteractionRequest](/docs/api/type-aliases/revturbinetreatmentinteractionrequest/) - [RevTurbineTrialContext](/docs/api/type-aliases/revturbinetrialcontext/) - [RevTurbineTriggerEvent](/docs/api/type-aliases/revturbinetriggerevent/) - [RevTurbineTriggerPayload](/docs/api/type-aliases/revturbinetriggerpayload/) - [RevTurbineUiPathActionTypes](/docs/api/type-aliases/revturbineuipathactiontypes/) - [RevTurbineUiPathResolver](/docs/api/type-aliases/revturbineuipathresolver/) - [RevTurbineUiPathResolverMap](/docs/api/type-aliases/revturbineuipathresolvermap/) - [RevTurbineUpdateInput](/docs/api/type-aliases/revturbineupdateinput/) - [RevTurbineUsageSnapshot](/docs/api/type-aliases/revturbineusagesnapshot/) - [SdkEventProperties](/docs/api/type-aliases/sdkeventproperties/) - [SdkMetadata](/docs/api/type-aliases/sdkmetadata/) - [SdkSessionOptions](/docs/api/type-aliases/sdksessionoptions/) - [SdkTraits](/docs/api/type-aliases/sdktraits/) - [ServerActionHandler](/docs/api/type-aliases/serveractionhandler/) - [ServerActionMap](/docs/api/type-aliases/serveractionmap/) - [ServerEntitlementResult](/docs/api/type-aliases/serverentitlementresult/) - [ServerEvaluationHydrationPayload](/docs/api/type-aliases/serverevaluationhydrationpayload/) - [ServerPlacementDecision](/docs/api/type-aliases/serverplacementdecision/) - [ServerUserContext](/docs/api/type-aliases/serverusercontext/) - [SlotRouteParams](/docs/api/type-aliases/slotrouteparams/) - [SurfaceSlotCategory](/docs/api/type-aliases/surfaceslotcategory/) - [TelemetryConsent](/docs/api/type-aliases/telemetryconsent/) - [ToastComponentProps](/docs/api/type-aliases/toastcomponentprops/) - [TrackFn](/docs/api/type-aliases/trackfn/) - [TrackPurpose](/docs/api/type-aliases/trackpurpose/) - [UnvalidatedConfigArtifact](/docs/api/type-aliases/unvalidatedconfigartifact/) - [UsageBalances](/docs/api/type-aliases/usagebalances/) - [UsePlacementOptions](/docs/api/type-aliases/useplacementoptions/) - [UsePlacementResult](/docs/api/type-aliases/useplacementresult/) - [UserContextInput](/docs/api/type-aliases/usercontextinput/) ## Variables - [CHECKOUT\_STARTED\_CTA\_ACTION\_TYPES](/docs/api/variables/checkout_started_cta_action_types/) - [CONTROL\_PLANE\_EVENT\_SOURCE](/docs/api/variables/control_plane_event_source/) - [CONTROL\_PLANE\_SOURCE\_KEY](/docs/api/variables/control_plane_source_key/) - [DARK\_THEME](/docs/api/variables/dark_theme/) - [DEFAULT\_BRANDING](/docs/api/variables/default_branding/) - [DEFAULT\_HOSTED\_ENDPOINT](/docs/api/variables/default_hosted_endpoint/) - [DEFAULT\_SDK\_MODE](/docs/api/variables/default_sdk_mode/) - [DEFAULT\_THEME](/docs/api/variables/default_theme/) - [EMITTABLE\_PLATFORM\_EVENT\_NAMES](/docs/api/variables/emittable_platform_event_names/) - [FALLBACK\_ACCOUNT\_ID\_PREFIX](/docs/api/variables/fallback_account_id_prefix/) - [FIXED\_BANNER\_TEMPLATE\_IDS](/docs/api/variables/fixed_banner_template_ids/) - [FIXED\_SURFACE\_TEMPLATE\_IDS](/docs/api/variables/fixed_surface_template_ids/) - [GATED\_SURFACE\_TEMPLATE\_IDS](/docs/api/variables/gated_surface_template_ids/) - [GENERAL\_BANNER\_TEMPLATE\_IDS](/docs/api/variables/general_banner_template_ids/) - [GENERAL\_MODAL\_TEMPLATE\_IDS](/docs/api/variables/general_modal_template_ids/) - [GENERAL\_TOAST\_TEMPLATE\_IDS](/docs/api/variables/general_toast_template_ids/) - [INIT\_STATUS\_OK](/docs/api/variables/init_status_ok/) - [MESSAGE\_SURFACE\_TEMPLATE\_IDS](/docs/api/variables/message_surface_template_ids/) - [MessageSurfaceSlot](/docs/api/variables/messagesurfaceslot/) - [RECOGNIZED\_UPDATE\_KEYS](/docs/api/variables/recognized_update_keys/) - [RuntimeMode](/docs/api/variables/runtimemode/) - [SERVER\_TRAITS\_DOMAIN](/docs/api/variables/server_traits_domain/) - [SLOT\_ROUTE\_ID\_PLACEHOLDER](/docs/api/variables/slot_route_id_placeholder/) - [SLOT\_ROUTE\_MAX\_LENGTH](/docs/api/variables/slot_route_max_length/) ## Functions - [AccessDeniedPlaceholder](/docs/api/functions/accessdeniedplaceholder/) - [adaptExperimentVersionToBucketer](/docs/api/functions/adaptexperimentversiontobucketer/) - [BannerComponent](/docs/api/functions/bannercomponent/) - [BannerFrame](/docs/api/functions/bannerframe/) - [baseThemeForScheme](/docs/api/functions/basethemeforscheme/) - [bridgeUiPathResolversIntoRegistry](/docs/api/functions/bridgeuipathresolversintoregistry/) - [bucketSubject](/docs/api/functions/bucketsubject/) - [buildControlPlaneEvent](/docs/api/functions/buildcontrolplaneevent/) - [ButtonComponent](/docs/api/functions/buttoncomponent/) - [categorizeActionError](/docs/api/functions/categorizeactionerror/) - [clearPersistedTheme](/docs/api/functions/clearpersistedtheme/) - [collectAssignmentFactDeclarations](/docs/api/functions/collectassignmentfactdeclarations/) - [CompactCircularGauge](/docs/api/functions/compactcirculargauge/) - [composeEffectiveExperimentContext](/docs/api/functions/composeeffectiveexperimentcontext/) - [createAnalyticsProvider](/docs/api/functions/createanalyticsprovider/) - [createBasicExperimentProvider](/docs/api/functions/createbasicexperimentprovider/) - [createChainedPlacementRequest](/docs/api/functions/createchainedplacementrequest/) - [createCompositeExperimentProvider](/docs/api/functions/createcompositeexperimentprovider/) - [createCustomEndpointRuntimeConfig](/docs/api/functions/createcustomendpointruntimeconfig/) - [createEntitlementPlacementRequest](/docs/api/functions/createentitlementplacementrequest/) - [createLocalRuntimeConfig](/docs/api/functions/createlocalruntimeconfig/) - [createNativeExperimentAssignmentProvider](/docs/api/functions/createnativeexperimentassignmentprovider/) - [createPostHogAnalyticsProvider](/docs/api/functions/createposthoganalyticsprovider/) - [createPostHogIntegration](/docs/api/functions/createposthogintegration/) - [createRevTurbineApiClient](/docs/api/functions/createrevturbineapiclient/) - [createSemanticEvent](/docs/api/functions/createsemanticevent/) - [createServerRuntimeConfig](/docs/api/functions/createserverruntimeconfig/) - [createSlotPlacementRequest](/docs/api/functions/createslotplacementrequest/) - [createStrictLocalRuntimeConfig](/docs/api/functions/createstrictlocalruntimeconfig/) - [createTreatmentInteraction](/docs/api/functions/createtreatmentinteraction/) - [currentBrowserRoute](/docs/api/functions/currentbrowserroute/) - [defineUiPathResolvers](/docs/api/functions/defineuipathresolvers/) - [dispatchCtaClick](/docs/api/functions/dispatchctaclick/) - [EmailPreviewComponent](/docs/api/functions/emailpreviewcomponent/) - [EngagementArea](/docs/api/functions/engagementarea/) - [entitlementResultDenies](/docs/api/functions/entitlementresultdenies/) - [fallbackAccountId](/docs/api/functions/fallbackaccountid/) - [FixedSurfaceSlot](/docs/api/functions/fixedsurfaceslot/) - [formatCurrencyMinorUnits](/docs/api/functions/formatcurrencyminorunits/) - [Gate](/docs/api/functions/gate/) - [getDefaultCtaResolverRegistry](/docs/api/functions/getdefaultctaresolverregistry/) - [getDefaultRegistry](/docs/api/functions/getdefaultregistry/) - [InitFailureDiagnostic](/docs/api/functions/initfailurediagnostic/) - [initRevTurbine](/docs/api/functions/initrevturbine/) - [initStatusForError](/docs/api/functions/initstatusforerror/) - [InlineCardPlacement](/docs/api/functions/inlinecardplacement/) - [InlineEmbedComponent](/docs/api/functions/inlineembedcomponent/) - [isExperimentAssignmentFactDeclaration](/docs/api/functions/isexperimentassignmentfactdeclaration/) - [isFallbackAccountId](/docs/api/functions/isfallbackaccountid/) - [isIdentifierLikeSegment](/docs/api/functions/isidentifierlikesegment/) - [loadTheme](/docs/api/functions/loadtheme/) - [mergeTheme](/docs/api/functions/mergetheme/) - [ModalComponent](/docs/api/functions/modalcomponent/) - [ModalFrame](/docs/api/functions/modalframe/) - [normalizeConfigArtifactOrThrow](/docs/api/functions/normalizeconfigartifactorthrow/) - [normalizeSlotRoute](/docs/api/functions/normalizeslotroute/) - [parsePromotion](/docs/api/functions/parsepromotion/) - [parseUiPath](/docs/api/functions/parseuipath/) - [Placement](/docs/api/functions/placement/) - [PlacementDecisionInspector](/docs/api/functions/placementdecisioninspector/) - [PlacementRenderer](/docs/api/functions/placementrenderer/) - [PushPreviewComponent](/docs/api/functions/pushpreviewcomponent/) - [QuotaMeterFrame](/docs/api/functions/quotameterframe/) - [registerBuiltinSlotTypes](/docs/api/functions/registerbuiltinslottypes/) - [registerBuiltinSnoozeResolver](/docs/api/functions/registerbuiltinsnoozeresolver/) - [registerCtaResolver](/docs/api/functions/registerctaresolver/) - [registerServerActionResolvers](/docs/api/functions/registerserveractionresolvers/) - [remediationFor](/docs/api/functions/remediationfor/) - [resetDefaultCtaResolverRegistry](/docs/api/functions/resetdefaultctaresolverregistry/) - [resetDefaultRegistry](/docs/api/functions/resetdefaultregistry/) - [resolveBranding](/docs/api/functions/resolvebranding/) - [resolveContent](/docs/api/functions/resolvecontent/) - [resolveLocalPlaybook](/docs/api/functions/resolvelocalplaybook/) - [resolvePersistentStorage](/docs/api/functions/resolvepersistentstorage/) - [resolveSessionStorage](/docs/api/functions/resolvesessionstorage/) - [resolveTokens](/docs/api/functions/resolvetokens/) - [RevTurbineProvider](/docs/api/functions/revturbineprovider/) - [RevTurbineRoute](/docs/api/functions/revturbineroute/) - [RevTurbineThemeProvider](/docs/api/functions/revturbinethemeprovider/) - [routePatternFromParams](/docs/api/functions/routepatternfromparams/) - [Slot](/docs/api/functions/slot/) - [SmsPreviewComponent](/docs/api/functions/smspreviewcomponent/) - [TelemetryScope](/docs/api/functions/telemetryscope/) - [Track](/docs/api/functions/track/) - [TrackOnView](/docs/api/functions/trackonview/) - [unregisterCtaResolver](/docs/api/functions/unregisterctaresolver/) - [useAddons](/docs/api/functions/useaddons/) - [useCan](/docs/api/functions/usecan/) - [useEntitlement](/docs/api/functions/useentitlement/) - [useGatedAction](/docs/api/functions/usegatedaction/) - [usePlacement](/docs/api/functions/useplacement/) - [usePlacementPersonalization](/docs/api/functions/useplacementpersonalization/) - [usePlans](/docs/api/functions/useplans/) - [useResolvedColorScheme](/docs/api/functions/useresolvedcolorscheme/) - [useRevTurbineTheme](/docs/api/functions/userevturbinetheme/) - [useRevTurbineThemeProviderPresent](/docs/api/functions/userevturbinethemeproviderpresent/) - [UserProfile](/docs/api/functions/userprofile/) - [useSlotRoute](/docs/api/functions/useslotroute/) - [useSurfaceSlot](/docs/api/functions/usesurfaceslot/) - [useTelemetryProps](/docs/api/functions/usetelemetryprops/) - [useTelemetryScope](/docs/api/functions/usetelemetryscope/) - [useTrack](/docs/api/functions/usetrack/) - [useTrackedAction](/docs/api/functions/usetrackedaction/) ## References ### ~~BannerSlot~~ Renames and re-exports [BannerComponent](/docs/api/functions/bannercomponent/) *** ### ~~BannerSlotProps~~ Renames and re-exports [BannerComponentProps](/docs/api/type-aliases/bannercomponentprops/) *** ### ~~ButtonSlot~~ Renames and re-exports [ButtonComponent](/docs/api/functions/buttoncomponent/) *** ### ~~ButtonSlotProps~~ Renames and re-exports [ButtonComponentProps](/docs/api/type-aliases/buttoncomponentprops/) *** ### ~~CliSlotProps~~ Renames and re-exports [CliComponentProps](/docs/api/type-aliases/clicomponentprops/) *** ### ~~CreditBalanceSlotProps~~ Renames and re-exports [CreditBalanceComponentProps](/docs/api/type-aliases/creditbalancecomponentprops/) *** ### ~~EmailPreviewSlot~~ Renames and re-exports [EmailPreviewComponent](/docs/api/functions/emailpreviewcomponent/) *** ### ~~FullPageSlotProps~~ Renames and re-exports [FullPageComponentProps](/docs/api/type-aliases/fullpagecomponentprops/) *** ### ~~InlineEmbedSlot~~ Renames and re-exports [InlineEmbedComponent](/docs/api/functions/inlineembedcomponent/) *** ### ~~InlineEmbedSlotProps~~ Renames and re-exports [InlineEmbedComponentProps](/docs/api/type-aliases/inlineembedcomponentprops/) *** ### ~~InPageSlotProps~~ Renames and re-exports [InPageComponentProps](/docs/api/type-aliases/inpagecomponentprops/) *** ### ~~ModalSlot~~ Renames and re-exports [ModalComponent](/docs/api/functions/modalcomponent/) *** ### ~~ModalSlotProps~~ Renames and re-exports [ModalComponentProps](/docs/api/type-aliases/modalcomponentprops/) *** ### ~~PushPreviewSlot~~ Renames and re-exports [PushPreviewComponent](/docs/api/functions/pushpreviewcomponent/) *** ### ~~QuotaMeterSlotProps~~ Renames and re-exports [QuotaMeterComponentProps](/docs/api/type-aliases/quotametercomponentprops/) *** ### ~~RTSlot~~ Renames and re-exports [Slot](/docs/api/functions/slot/) *** ### ~~SmsPreviewSlot~~ Renames and re-exports [SmsPreviewComponent](/docs/api/functions/smspreviewcomponent/) *** ### ~~ToastSlotProps~~ Renames and re-exports [ToastComponentProps](/docs/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`](/docs/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`](/docs/api/type-aliases/analyticseventproperties/); \} \| `null` Optional transformer applied before the handler. Return `null` to drop the event silently. ## Parameters ### eventName `string` ### properties [`AnalyticsEventProperties`](/docs/api/type-aliases/analyticseventproperties/) ## Returns \{ `eventName`: `string`; `properties`: [`AnalyticsEventProperties`](/docs/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`](/docs/api/interfaces/placementslotprops/) & `object` Props for [BannerSlot](/docs/api/functions/bannercomponent/). Extends [PlacementSlotProps](/docs/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`](/docs/api/type-aliases/configartifact/); `playbook`: [`ConfigArtifact`](/docs/api/type-aliases/configartifact/); \} \| \{ `exportedConfig`: [`ConfigArtifact`](/docs/api/type-aliases/configartifact/); `playbook?`: [`ConfigArtifact`](/docs/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). # ButtonComponentProps > **ButtonComponentProps** = [`PlacementSlotProps`](/docs/api/interfaces/placementslotprops/) & `object` Props for [ButtonSlot](/docs/api/functions/buttoncomponent/). Extends [PlacementSlotProps](/docs/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`](/docs/api/interfaces/placementslotprops/) Props for CliSlot. Uses the standard [PlacementSlotProps](/docs/api/interfaces/placementslotprops/) without extensions. # ConfigArtifact > **ConfigArtifact** = `Playbook` \| `RevTurbineConfig` A canonical Playbook or the deprecated RevTurbineConfig wire shape. # CreditBalanceComponentProps > **CreditBalanceComponentProps** = [`PlacementSlotProps`](/docs/api/interfaces/placementslotprops/) & `object` Props for CreditBalanceSlot. Extends [PlacementSlotProps](/docs/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`](/docs/api/interfaces/placementuipath/) ### context [`CtaResolverContext`](/docs/api/interfaces/ctaresolvercontext/) ## Returns `void` # 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](/docs/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`](/docs/api/interfaces/revturbineconfigprovider/) :::caution[Deprecated] Renamed to [RevTurbineConfigProvider](/docs/api/interfaces/revturbineconfigprovider/) (plan 104). Kept as a back-compat alias so existing integrations keep compiling. Removed in `0.12.0` alongside the other `ExportedConfig`-spelled aliases (BL-0156). Use `RevTurbineConfigProvider`. ::: # FullPageComponentProps > **FullPageComponentProps** = [`PlacementSlotProps`](/docs/api/interfaces/placementslotprops/) & `object` Props for FullPageSlot. Extends [PlacementSlotProps](/docs/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`](/docs/api/type-aliases/usercontextinput/), `ServerAssignedUserContextKey`\>\> & `object` Input accepted by [RevTurbineCustomerSdk.identify](/docs/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. ## Type Declaration ### plan\_handle? > `optional` **plan\_handle?**: `string` THE plan matching identity — the plan's `unique_handle` (e.g. `'pro'`). # InlineEmbedComponentProps > **InlineEmbedComponentProps** = [`PlacementSlotProps`](/docs/api/interfaces/placementslotprops/) Props for [InlineEmbedSlot](/docs/api/functions/inlineembedcomponent/). # InPageComponentProps > **InPageComponentProps** = [`InlineEmbedComponentProps`](/docs/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`](/docs/api/interfaces/configtargetdefaults/) :::caution[Deprecated] Renamed to [ConfigTargetDefaults](/docs/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`](/docs/api/interfaces/placementslotprops/) & `object` Props for [ModalSlot](/docs/api/functions/modalcomponent/). Extends [PlacementSlotProps](/docs/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](/docs/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`](/docs/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](/docs/api/type-aliases/ctaresolver/) can be registered against it via `registerCtaResolver`. # PlaybookProvider > **PlaybookProvider** = [`RevTurbineConfigProvider`](/docs/api/interfaces/revturbineconfigprovider/) The Playbook-named counterpart of the deprecated [ExportedConfigProvider](/docs/api/type-aliases/exportedconfigprovider/) alias (BL-0156). Identical to [RevTurbineConfigProvider](/docs/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`](/docs/api/interfaces/placementslotprops/) & `object` Props for QuotaMeterSlot. Extends [PlacementSlotProps](/docs/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](/docs/api/type-aliases/identifycontextinput/) keys [RevTurbineCustomerSdk.identify](/docs/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`](/docs/api/variables/recognized_update_keys/)\[`number`\] The top-level [RevTurbineUpdateInput](/docs/api/type-aliases/revturbineupdateinput/) keys [RevTurbineCustomerSdk.update](/docs/api/classes/revturbinecustomersdk/#update) patches (the entries of [RECOGNIZED\_UPDATE\_KEYS](/docs/api/variables/recognized_update_keys/)). Exported for type-level exhaustiveness checks; it has no runtime value. # RegisterPlacementSlotTypeOptions > **RegisterPlacementSlotTypeOptions**\<`P`\> = `Omit`\<[`PlacementSlotType`](/docs/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`](/docs/api/interfaces/placementslotprops/) = [`PlacementSlotProps`](/docs/api/interfaces/placementslotprops/) # ResolvedContent > **ResolvedContent** = [`PlacementContentFields`](/docs/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`](/docs/api/interfaces/revturbineconfigprovider/) & \{ `getPlaybook`: [`ConfigArtifact`](/docs/api/type-aliases/configartifact/) \| `undefined`; \} \| \{ `getExportedConfig`: [`ConfigArtifact`](/docs/api/type-aliases/configartifact/) \| `undefined`; \} A [RevTurbineConfigProvider](/docs/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](/docs/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`](/docs/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](/docs/api/functions/resolvelocalplaybook/). ## Type Declaration ### apiKey? > `optional` **apiKey?**: `string` ### endpoint? > `optional` **endpoint?**: `string` ### localRuntime > **localRuntime**: `RevTurbineLocalRuntimeOptions` & \{ `playbook`: [`ConfigArtifact`](/docs/api/type-aliases/configartifact/) \| [`UnvalidatedConfigArtifact`](/docs/api/type-aliases/unvalidatedconfigartifact/); \} \| \{ `exportedConfig`: [`ConfigArtifact`](/docs/api/type-aliases/configartifact/) \| [`UnvalidatedConfigArtifact`](/docs/api/type-aliases/unvalidatedconfigartifact/); \} ### mode? > `optional` **mode?**: [`RevTurbineSdkMode`](/docs/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`](/docs/api/variables/runtimemode/)\[keyof *typeof* [`RuntimeMode`](/docs/api/variables/runtimemode/)\] The set of valid runtime modes (plan 144), derived from [RuntimeMode](/docs/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](/docs/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`](/docs/api/interfaces/trialtriggerpayload/) \| [`UsageTriggerPayload`](/docs/api/interfaces/usagetriggerpayload/) \| [`FeatureGateTriggerPayload`](/docs/api/interfaces/featuregatetriggerpayload/) \| [`PaymentTriggerPayload`](/docs/api/interfaces/paymenttriggerpayload/) \| [`SdkEventProperties`](/docs/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`](/docs/api/type-aliases/revturbineuipathresolver/)\>\> Map of `action_type` -> resolver implementation supplied by the customer app. # RevTurbineUpdateInput > **RevTurbineUpdateInput** = `Omit`\<[`RevTurbineUserContext`](/docs/api/interfaces/revturbineusercontext/), `"id"` \| `"usage"`\> & `object` Input to [RevTurbineCustomerSdk.update](/docs/api/classes/revturbinecustomersdk/#update) — the advertised `update(patch)` verb. Accepts **every session-scoped user-context field** — everything on [RevTurbineUserContext](/docs/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](/docs/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](/docs/api/interfaces/revturbineusercontext/). ## Type Declaration ### usage? > `optional` **usage?**: [`UsageBalances`](/docs/api/type-aliases/usagebalances/) Usage balances to merge into the current snapshot (absolute values). # RevTurbineUsageSnapshot > **RevTurbineUsageSnapshot** = `Record`\<`string`, [`RevTurbineUsageSnapshotEntry`](/docs/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`](/docs/api/type-aliases/revturbineinitinputoptions/) & `object` Options for [initRevTurbine](/docs/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). # ServerActionHandler > **ServerActionHandler** = (`context`) => `Promise`\<[`ServerActionResult`](/docs/api/interfaces/serveractionresult/)\> App handler that calls its own backend for a server-owned mutation. ## Parameters ### context [`ServerActionContext`](/docs/api/interfaces/serveractioncontext/) ## Returns `Promise`\<[`ServerActionResult`](/docs/api/interfaces/serveractionresult/)\> # ServerActionMap > **ServerActionMap** = `Record`\<`string`, [`ServerActionHandler`](/docs/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](/docs/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`](/docs/api/interfaces/placementslotprops/) & `object` Props for ToastSlot. Extends [PlacementSlotProps](/docs/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](/docs/api/functions/usetrack/). ## Parameters ### name `string` ### data? [`SdkEventProperties`](/docs/api/type-aliases/sdkeventproperties/) ### options? [`TrackOptions`](/docs/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](/docs/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](/docs/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](/docs/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](/docs/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](/docs/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`](/docs/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](/docs/api/type-aliases/useplacementresult/#exposureref) to the visual root to use it. Read at mount. *** ### surfaceSlot? > `optional` **surfaceSlot?**: [`RevTurbineSurfaceSlotConfig`](/docs/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](/docs/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](/docs/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](/docs/api/functions/buildcontrolplaneevent/) stamps the source classification on an emitted event's properties bag. # DARK_THEME > `const` **DARK\_THEME**: `Readonly`\<[`RevTurbineTheme`](/docs/api/interfaces/revturbinetheme/)\> Dark counterpart to [DEFAULT\_THEME](/docs/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](/docs/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`](/docs/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`](/docs/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](/docs/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](/docs/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`](/docs/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"`, `"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](/docs/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](/docs/components/button-slot/) | `button` | Standalone CTA button for nav bars and inline prompts | | [BannerSlot](/docs/components/banner-slot/) | `banner` | Full-width banner for announcements and usage warnings | | [ModalSlot](/docs/components/modal-slot/) | `modal` | Overlay modal for feature gates and trial expirations | | [ToastSlot](/docs/components/toast-slot/) | `toast` | Ephemeral toast notification for usage alerts | | [QuotaMeterSlot](/docs/components/quota-meter-slot/) | `in_page` | Usage meter with progress bar, counter, and gauge styles | | [InlineEmbedSlot](/docs/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](/docs/playground/#fixed-banner) · [API Reference](/docs/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](/docs/guides/placements/#fixed-button) · [API Reference](/docs/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](/docs/playground/#gate-inline) · [API Reference](/docs/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](/docs/guides/entitlements/#gate-modal) · [API Reference](/docs/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](/docs/playground/#fixed-usage-counter) · [API Reference](/docs/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](/docs/playground/#msg-toast) · [API Reference](/docs/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](/docs/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](/docs/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](/docs/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()`](/docs/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 the UI the control plane decides to render at the right moment: an upgrade prompt when a gate denies, a "running low" banner near a limit, a trial-ending toast. You render one with a [``](/docs/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 `` `user` prop. Entitlement checks and [segment targeting](/docs/guides/experiments/) both read from it. The dedicated [`RevTurbineUserContext` guide](/docs/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](/docs/guides/runtime-modes/). ## Where decisions run **Tip — No latency, no dependence on our availability:** 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. 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](/docs/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 (``) | For the component/config name aliases (`SurfaceSlotComponent`, `Playbook`, …), see the [Recommended API Path](/docs/getting-started/api-overview/#the-vocabulary-slot--gate--playbook). ## Related - [Recommended API Path](/docs/getting-started/api-overview/) — which call to reach for - [Client vs Server Enforcement](/docs/concepts/enforcement/) — policy vs enforcement - [Entitlements](/docs/guides/entitlements/) · [Placements](/docs/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](/docs/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](/docs/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'; // Server: the trusted decision. Nothing on this path calls RevTurbine — // the headless SDK evaluates the Playbook it holds. async function handleExport(req, res) { const rt = await initRevTurbine({ tenantId: process.env.REVTURBINE_TENANT_ID!, apiKey: process.env.REVTURBINE_API_KEY!, endpoint: process.env.REVTURBINE_ENDPOINT!, runtimeMode: RuntimeMode.Server, user: { id: req.userId, plan_handle: req.user.planHandle }, }); const result = await rt.can('data_export'); if (!result.allowed) return res.status(402).json({ error: 'upgrade_required' }); await runExport(req); } ``` 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 on a request hot path, don't pay the Playbook fetch per request — see [Instance lifecycle](/docs/getting-started/server-side/#instance-lifecycle). 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](/docs/getting-started/api-overview/) — when to reach for `can()` vs `gate()` vs `` - [Server-Side Integration](/docs/getting-started/server-side/) — running entitlement checks on your backend - [Entitlements](/docs/guides/entitlements/) — the entitlement model behind every check - [Error Handling](/docs/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](/docs/guides/runtime-modes/) — choosing the right runtime mode - [Component Gallery](/docs/components/) — interactive demos of every built-in slot - [API Reference](/docs/api/) — full TypeDoc reference for all exports # 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](/docs/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](/docs/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](/docs/concepts/enforcement/) — the policy-vs-enforcement split in detail - [Placements](/docs/guides/placements/) — slots, CTA actions, and the empty-state rule - [Entitlements](/docs/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 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](/docs/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](/docs/) — gate one action end to end - [Entitlements](/docs/guides/entitlements/) — the full entitlement model behind `can()` / `gate()` - [Placements](/docs/guides/placements/) — what `` renders and how decisions resolve # 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](/docs/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](/docs/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](/docs/) — render your first placement - [Runtime modes](/docs/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](/docs/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. Construct a **new** `RevTurbineCustomerSdk` with the new snapshot. Because the instance is stateless, "refresh" is just "rebuild" — atomically swap the reference your handlers read. ```python # e.g. behind a lock / atomic reference, refreshed every N minutes _sdk = RevTurbineCustomerSdk(user_context=ctx, exported_config=load_config()) ``` 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](/docs/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](/docs/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](/docs/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](/docs/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 | ### 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](/docs/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](/docs/guides/entitlements/) — feature gates, usage limits, and credits - [Placements Guide](/docs/guides/placements/) — slot-based and entitlement-based patterns - [Theming Guide](/docs/guides/theming/) — customize slot component appearance - [Interactive Playground](/docs/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](/docs/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](/docs/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. In `Server` mode it fetches the launched Playbook and evaluates each entitlement and placement in-process; in `LocalOnly` mode you bundle the Playbook yourself and make no decisioning network calls. **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](/docs/user-context/#server-sdk-context-shapes). ## When to Use Server-Side | Use case | Client SDK | Server SDK | |---|---|---| | Render placements in React | ✅ | — | | Gate API endpoints | — | ✅ | | Enforce usage limits before processing | — | ✅ | | SSR placement decisions | ✅ (local mode) | ✅ | | Track server-side events | — | ✅ | ## 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: ```ts import { initRevTurbine, RuntimeMode, type RevTurbineUserContext, } from '@revturbine/sdk/headless'; async function handleExport(req) { const user: RevTurbineUserContext = { id: req.user.id, plan_handle: req.user.planHandle, usage: { api_calls: { entitlement_handle: 'api_calls', unit: 'calls', amount: req.user.apiCallCount, limit: req.user.apiCallLimit, }, }, }; const rt = await initRevTurbine({ tenantId: process.env.REVTURBINE_TENANT_ID!, apiKey: process.env.REVTURBINE_API_KEY!, endpoint: process.env.REVTURBINE_ENDPOINT!, runtimeMode: RuntimeMode.Server, user, }); const result = await rt.can('data_export'); if (!result.allowed) { return new Response('Upgrade required', { status: 402 }); } await rt.track('data_export_started'); await rt.flushEvents(); return runExport(req); } ``` `Server` mode fetches the launched Playbook from the control plane, then evaluates locally. Before the first Playbook has arrived, a check denies rather than grants; once it has, a failed refresh keeps the last-known-good Playbook. ### Instance lifecycle Each `initRevTurbine` call in `Server` mode fetches the launched Playbook and then caches it for that instance, re-checking for a new release at most once a minute. A request-scoped instance therefore pays that fetch on every request. Two patterns avoid it: - **One long-lived instance per process** where a single user context per instance fits — workers, cron jobs, a per-tenant service. - **A shared Playbook snapshot with a request-scoped `LocalOnly` instance** — the model the Python and Rust SDKs use. Load the Playbook once per process (`revturbine download --live` on deploy, or a timer), then construct `initRevTurbine({ runtimeMode: RuntimeMode.LocalOnly, localRuntime: { playbook }, user })` per request, as in [Bundled Local Runtime](#bundled-local-runtime) below. Note that a `LocalOnly` instance does not send events to RevTurbine ingest. 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. ## 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 import { initRevTurbine, RuntimeMode } from '@revturbine/sdk/headless'; export async function POST(request: Request) { const user = await requireUser(request); const rt = await initRevTurbine({ tenantId: process.env.REVTURBINE_TENANT_ID!, apiKey: process.env.REVTURBINE_API_KEY!, endpoint: process.env.REVTURBINE_ENDPOINT!, runtimeMode: RuntimeMode.Server, user: { id: user.id, plan_handle: user.planHandle }, }); const access = await rt.can('trial_extension'); if (!access.allowed) return new Response('Upgrade required', { status: 402 }); const trial = await extendTrial(user.id); return Response.json({ plan_handle: user.planHandle, trial: { in_trial: true, days_remaining: trial.daysRemaining }, }); } ``` 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: ```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`, mints short-lived, browser-safe client sessions from a secret server key. It is available today, but it is not the decision evaluator; use the headless session above for checks. ## 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](/docs/guides/headless-api/) | | Python | `revturbine` (PyPI) | [Python SDK](/docs/getting-started/python/) | | Rust | `revturbine` (crates.io) | [Rust SDK](/docs/getting-started/rust/) | TypeScript supports local and hosted modes. 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](/docs/getting-started/python/) and [Rust SDK guide](/docs/getting-started/rust/) for setup and usage. ## Next Steps - [User Context](/docs/user-context/) — canonical fields and cross-language shapes - [Entitlements Guide](/docs/guides/entitlements/) — entitlement types and checking patterns - [Runtime Modes](/docs/guides/runtime-modes/) — choosing the right runtime mode - [Headless API](/docs/guides/headless-api/) — imperative SDK usage without React # 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](/docs/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 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()`](/docs/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](/docs/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](/docs/guides/coding-agent/). ## Related - [Placements](/docs/guides/placements/) — decision inputs, caps, and priority in depth - [Run an Experiment with Segments](/docs/guides/experiments/) - [Debugging Decisions](/docs/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. ### Linking a Stripe customer to a RevTurbine user RevTurbine identifies a user by the `user.id` your app passes to the SDK. To tie that user to their Stripe customer, put the same id on the Stripe Customer object as `metadata.revturbine_user_id` when you create it (for example, when your checkout creates the customer). RevTurbine links the two on `customer.created`, and uses the same key to attribute conversions to the prompts that drove them. ```ts // Your checkout, when it creates the Stripe customer const customer = await stripe.customers.create({ email: user.email, metadata: { revturbine_user_id: user.id }, // the id you pass to the SDK }); ``` If a Stripe customer was created without the metadata, RevTurbine can still match it once it has seen the customer's Stripe id — but the metadata is the explicit, authoritative join, and the one to rely on. ### 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 // Browser: `output` is the decision the slot rendered (`decision.output`). 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. # 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](/docs/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](/docs/guides/runtime-modes/) and [Client vs Server Enforcement](/docs/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](/docs/reference/cli/). Treat `revturbine --help` from the project-pinned CLI as authoritative. ## Related - [Replace Hard-Coded Plan Checks](/docs/guides/migrate-plan-checks/) — the before/after the agent produces - [Beyond Gating](/docs/guides/beyond-gating/) — what the config can express - [Client vs Server Enforcement](/docs/concepts/enforcement/) — the paths the agent must not touch # 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](/docs/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](/docs/guides/theming/) — customize colors, typography, and shapes - [Component Gallery](/docs/components/) — built-in slot demos - [API Reference](/docs/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](/docs/playground/#access-gates): [Branding gate](/docs/playground/#gate-inline) and [Brand Kit gate](/docs/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](/docs/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](/docs/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 two entitlement types always evaluate to allowed — the one explicit carve-out from fail-closed. They are **non-gating metadata** that exists to populate text tokens (an overage price in upgrade copy, a documented rate ceiling), not to control access. Gating a feature on one of them grants it to everyone. ::: ## Next Steps - [Placements Guide](/docs/guides/placements/) — render upgrade prompts for denied entitlements - [Events & Analytics](/docs/guides/events/) — track entitlement check outcomes - [Error Handling](/docs/guides/error-handling/) — fail-closed checks and recovery patterns - [Try it live → Data Export Gate](/docs/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](/docs/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](/docs/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. | ### 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` entitlements always evaluate to allowed. This is by design and is the one explicit carve-out from the fail-closed rule: they are **non-gating metadata**, present to populate text tokens (an overage price in upgrade copy, a documented rate ceiling), not to control access. Do not gate a feature on one of them — a `price_per_unit` check answering "allowed" tells you the price exists, not that the user may proceed. ::: ## 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](/docs/reference/configuration/) — error-related configuration options - [Error Codes Reference](/docs/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. ## 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_started` · `trial_expiring` · `trial_expired` · `usage_limit_approaching` · `usage_limit_exceeded` · `feature_gated` · `payment_retry_required` · `subscription_renewing`. :::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 const rt = initRevTurbine({ tenantId: 'your-tenant', publicKey: 'rtk_…', // 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) is a *server-side decisioning* control — it governs **who is eligible to see** a placement, and stamps its own marker on decision responses. 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](/docs/guides/placements/) — placement lifecycle and interactions - [Runtime Modes](/docs/guides/runtime-modes/) — local vs hosted mode - [Error Handling](/docs/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 const selection = await rt.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 rt.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 const rt = initRevTurbine({ tenantId: 'tenant_abc', publicKey: 'rtk_…', // 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](/docs/guides/placements/) — how placement payloads, targeting, and priority resolve - [Entitlements](/docs/guides/entitlements/) — entitlement rules and precedence - [Events & Analytics](/docs/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 const session = await initRevTurbine({ tenantId: 'tenant_abc', publicKey: 'rtk_…', // 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](/docs/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](/docs/playground/#headless-api): [PlacementController](/docs/playground/#headless-placement) and [SdkSession one-shot](/docs/playground/#headless-session). **EntitlementGate** — an entitlement check without React. _Live example: /playground/#headless-gate_ # 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'; async function handleExport(req, res) { const rt = await initRevTurbine({ tenantId: process.env.REVTURBINE_TENANT_ID!, apiKey: process.env.REVTURBINE_API_KEY!, endpoint: process.env.REVTURBINE_ENDPOINT!, runtimeMode: RuntimeMode.Server, user: { id: req.userId, plan_handle: req.user.planHandle }, }); const result = await rt.can('data_export'); if (!result.allowed) return res.status(402).json({ error: 'upgrade_required' }); await runExport(req); } ``` ```tsx // 2. The upgrade navigation, wired to the placement the renders when denied. router.push(path.url ?? '/pricing')}> ``` See [Client vs Server Enforcement](/docs/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](/docs/getting-started/api-overview/) — `can()` vs `gate()` vs `` - [Entitlements](/docs/guides/entitlements/) — feature / usage-limit / credits / seats rules - [Run an Experiment with Segments](/docs/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 [``](/docs/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](/docs/playground/#msg-banner) ## Modal ```tsx ``` > [Try it live → Data Export Gate](/docs/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](/docs/guides/placements/#fixed-button) ## Full-Page ```tsx ``` ## CLI ```tsx ``` ## Quota Meter (In-Page) ```tsx ``` > [Try it live → Quota Meter](/docs/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](/docs/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](/docs/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](/docs/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](/docs/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](/docs/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](/docs/playground/). **Upgrade Button** — an always-visible CTA in a nav slot. _Live example: /playground/#fixed-button_ More on the Playground: [Plans & Pricing Card](/docs/playground/#fixed-in-page) · [Quota Meter](/docs/playground/#fixed-usage-counter) · [Annual Nudge Banner](/docs/playground/#fixed-banner) · [Usage Warning Banner](/docs/playground/#msg-banner) · [Usage Exhausted Modal](/docs/playground/#msg-modal) · [Trial Welcome Toast](/docs/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](/docs/guides/placement-cookbook/#route-surface) in the Placement Cookbook for the `` boundary, the Next.js recipe, and the pathname fallback. ## Next Steps - [Component Gallery](/docs/components/) — interactive demos of every built-in slot component - [Custom Slot Types](/docs/guides/custom-slots/) — register your own slot components - [Events & Analytics](/docs/guides/events/) — track impressions and interactions - [Interactive Playground](/docs/playground/) — try all slot types live # 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`). ## 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 | `tenantId`, `publicKey`, `endpoint` | | **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](/docs/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](/docs/getting-started/react/) and let it initialize the SDK. See [User Context](/docs/user-context/), [CTA actions](/docs/guides/placements/#cta-actions) and [Theming](/docs/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. They are not a resilience mechanism for `revturbine_server` mode: decisions are evaluated locally from the cached Playbook, so RevTurbine's availability never affects them. 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](/docs/getting-started/server-side/). 4. If you supply your own facts through providers, choose `providerFailureSlotBehavior` per surface. # 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](/docs/guides/custom-slots/) — build components that use the theme - [Component Gallery](/docs/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](/docs/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](/docs/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. | ## 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](/docs/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 ``` # 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 const explanation = await rt.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](/docs/guides/placements/) — the decision input and how placements resolve - [Run an Experiment with Segments](/docs/guides/experiments/) — segment predicates in practice - [Production Readiness](/docs/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](/docs/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; cannot call management APIs. Create it at **Settings → API tokens → Ingest keys**. | | **Server key** | `rtk_…` (type `server`) | Your backend only | Programmatic calls to the RevTurbine API from your server — Playbook delivery, management APIs — and minting the session keys below. Refused on a browser-like request. Create it at **Settings → API tokens → 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` — on the server SDK and on the headless SDK alike; the sample on [Server-Side Integration](/docs/getting-started/server-side/) reads it from `REVTURBINE_API_KEY` — and use it to mint session keys. - **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](/docs/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 tokens → MCP**. 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](/docs/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](/docs/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](/docs/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](/docs/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](/docs/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](/docs/operate/debugging/):** use `explainPlacementDecision()` to see exactly why a decision resolved the way it did. ## Related - [Client vs Server Enforcement](/docs/concepts/enforcement/) - [What Owns What](/docs/concepts/source-of-truth/) - [Debugging Decisions](/docs/operate/debugging/) - [Error Handling](/docs/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. ## 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](/docs/getting-started/installation/) — SDK and CLI setup - [Entitlements](/docs/guides/entitlements/) — use generated handles in checks - [Runtime modes](/docs/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](/docs/getting-started/installation/) — installation instructions - [Configuration Reference](/docs/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 Required in `revturbine_server` and `custom_endpoints` modes. In `local_only` mode both are optional — `initRevTurbine({ runtimeMode: 'local_only', localRuntime: { playbook } })` is a complete configuration. | Field | Type | Description | |---|---|---| | `tenantId` | `string` | Your RevTurbine tenant identifier | | `publicKey` | `string` | In the browser: your public key (`rtk_…`, type `public`, minted under **Settings → API tokens → Ingest keys**). Safe to ship in a bundle. See [Production Readiness → Keys](/docs/operate/production-readiness/#keys). | | `apiKey` | `string` | On a server only: your server key (`rtk_…`, type `server`), the same option the server SDK takes. 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 resolves decisions | | `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](/docs/guides/theming/) for an example. Light/dark selection is the React provider's `colorScheme` prop, not an initialization option. ### 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`). 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 initRevTurbine({ tenantId: 'tenant_abc', publicKey: 'rtk_…', // public key — safe in the browser user: { id: 'user_123', plan_handle: 'free' }, clientSession: () => fetch('/api/revturbine-session', { method: 'POST' }) .then((r) => r.json()) .then((j) => j.client_token), }); ``` 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` | | `placementsByLookupKey` | `Record` — **deprecated, ignored since 0.11.21**; removed in 0.12.0 | | `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. Pre-decided outputs in `placementsByLookupKey` are ignored (with a warning); 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](/docs/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; // ✅ Required publicKey: string; // ✅ Required in the browser (public key); on a server pass the server key as `apiKey` 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; // ✅ Required publicKey: string; // ✅ Required (public key in the browser; `apiKey` = server key on a server) 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](/docs/guides/runtime-modes/) — mode comparison and migration - [Error Codes Reference](/docs/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. | :::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**](/docs/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](/docs/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](/docs/guides/error-handling/) — patterns and strategies # 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](/docs/)) ## 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](/docs/)). 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](/docs/tutorials/low-credits-warning/) and [Recover a Failed Payment](/docs/tutorials/payment-recovery/). ## Try It Live > [Interactive Playground → Banner Slot](/docs/playground/#fixed-banner) ## Next Steps - [Placements Guide](/docs/guides/placements/) — targeting, triggers, and surface types - [Tutorial: Add an Upgrade Button](/docs/tutorials/upgrade-button/) — a persistent CTA button instead of a banner - [Component Gallery](/docs/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](/docs/)) ## 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](/docs/guides/entitlements/#gate-modal) ## Next Steps - [Tutorial: Track Usage and Show a Quota Meter](/docs/tutorials/usage-quota-meter/) — usage-based entitlements - [Entitlements Guide](/docs/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](/docs/)) ## 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](/docs/). **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](/docs/guides/entitlements/). ## Try It Live > [Interactive Playground → Message Banner](/docs/playground/#msg-banner) ## Next Steps - [Entitlements Guide](/docs/guides/entitlements/) — credits, usage limits, and the `limited` state - [Tutorial: Track Usage and Show a Quota Meter](/docs/tutorials/usage-quota-meter/) — the usage-limit equivalent - [Component Gallery](/docs/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](/docs/)) - 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](/docs/playground/#fixed-banner) ## Next Steps - [Placements Guide](/docs/guides/placements/) — segments, targeting, and triggers - [Tutorial: Show a Banner Placement](/docs/tutorials/banner-placement/) — targeted banners - [Tutorial: Show a Trial-Ending Countdown](/docs/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](/docs/)) ## 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](/docs/). **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](/docs/playground/#fixed-banner) ## Next Steps - [Entitlements Guide](/docs/guides/entitlements/) — trials, usage limits, and credits - [Placements Guide](/docs/guides/placements/) — triggers and targeting - [Tutorial: Show a Banner Placement](/docs/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](/docs/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](/docs/guides/placements/#fixed-button) ## Next Steps - [Tutorial: Gate a Premium Feature](/docs/tutorials/gate-premium-feature/) — add an entitlement gate - [Component Gallery](/docs/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](/docs/)) ## 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 exceeds limit if (usagePercent >= 100) { await sdk.emitTrigger('usage_limit_exceeded', { entitlement_handle: 'api_calls', current_usage: currentCalls, usage_limit: 1000, }); } ``` ## Try It Live > [Interactive Playground → Quota Meter](/docs/playground/#fixed-usage-counter) ## Next Steps - [Entitlements Guide](/docs/guides/entitlements/) — all entitlement types - [Events & Analytics](/docs/guides/events/) — trigger events and tracking - [Component Gallery](/docs/components/) — interactive demos of all built-in slot components # 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](/docs/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](/docs/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](/docs/getting-started/server-side/), [Python guide](/docs/getting-started/python/), and [Rust guide](/docs/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'; export const rt = initRevTurbine({ tenantId: 'tenant_abc', publicKey: 'rtk_…', // public key — safe in the browser user: { id: 'user_123', plan_handle: 'free' }, clientSession: () => fetch('/api/revturbine/client-session', { method: 'POST' }) .then((response) => { if (!response.ok) throw new Error('Could not create RevTurbine client session'); return response.json(); }) .then((body) => body.client_token), }); ``` Your backend—not the browser—mints the short-lived `rt_client_…` token. 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](/docs/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](/docs/concepts/core-concepts/) — where user context fits in the decision loop - [React Integration](/docs/getting-started/react/) — provider and hook setup - [Configuration Reference](/docs/reference/configuration/) — initialization options - [Entitlements](/docs/guides/entitlements/) — how plan and usage become access decisions - [Run an Experiment](/docs/guides/experiments/) — direct and provider-owned assignments - [`RevTurbineUserContext` API reference](/docs/api/interfaces/revturbineusercontext/) — generated type details