Skip to content

Integration Points

RevTurbine is additive — it enhances the customer’s app but is never required for baseline UX. The customer’s app must look and function correctly without a placement payload. Surface slots return “nothing to show” by default; the app renders its standard UI. Placements layer conversion, expansion, and retention experiences on top.

RevTurbine is the authoritative source for access decisions on gated and usage-limited features. The customer’s app delegates entitlement checks to RT and enforces the result — no hard-coded access logic. Plan changes, entitlement updates, and placement configurations take effect immediately without code deploys.

MethodPurposeOutputs
rt.init(apiKey)Initialize SDK. Call once on startup.
rt.identify(userId, context)Identify user with plan, usage, and traits.
rt.getPlacement(config)Get the winning placement for a slot or entitlement.PlacementOutput | null
rt.checkEntitlement(handle, context?)Pure access check — feature, usage, credits, seats, tiers.{ status, currentTier?, reason? }
rt.updateUsage(balances)Update cached balances between identify calls.
rt.getTrialStatus()Get current trial state.{ inTrial, trialType, planHandle, dayNumber, daysRemaining }
rt.dismiss(outputId)User dismissed a placement.
rt.snooze(outputId)User snoozed a placement (“remind me later”).
rt.convert(outputId)User completed CTA.
rt.trackEvent(name, data?)Behavioral event for propensity scoring.
const p = rt.getPlacement({
slotId: "header_upgrade_cta",
surfaceType: "button"
});
if (p) renderUpgradeButton(p.content.label, p.cta_path);
else renderDefaultButton(); // additive only

2. Entitlement-Based (Gated, Usage/Credit/Seat)

Section titled “2. Entitlement-Based (Gated, Usage/Credit/Seat)”
const access = rt.checkEntitlement("ai_export");
if (access.status === "denied") {
const p = rt.getPlacement({ entitlementHandle: "ai_export" });
if (p) showModal(p);
else showGenericDenial();
} else {
startExport();
}
Entitlement TypeContext ParameterWhat the App PassesResponse Includes
FeatureNothing. Binary access.status
Capability Tier{ requiredTier }The tier level neededstatus, currentTier
Usage Limit{ used }Current consumed amountstatus
Credits{ balance }Remaining credit balancestatus
SeatNothing. RT knows from Stripe.status

Principle: The app passes only what it knows better than RT (current consumption, which tier is needed). RT handles everything it can derive from plan configuration and Stripe state.

const used = billing.getUsed("ai_credits");
const access = rt.checkEntitlement("ai_credits", { used });
if (access.status === "allowed") {
executeAction();
billing.recordUsage("ai_credits", 1);
rt.updateUsage({ ai_credits: billing.getUsed("ai_credits") });
} else if (access.status === "limited") {
executeAction(); // still allowed — approaching limit
billing.recordUsage("ai_credits", 1);
rt.updateUsage({ ai_credits: billing.getUsed("ai_credits") });
const p = rt.getPlacement({
slotId: "usage_warning",
surfaceType: "banner",
entitlementHandle: "ai_credits"
});
if (p) renderBanner(p);
} else {
const p = rt.getPlacement({
slotId: "usage_limit_gate",
surfaceType: "modal",
entitlementHandle: "ai_credits"
});
if (p) showModal(p);
else showGenericDenial();
}

Every placement payload includes a cta_path object. The type field determines the action:

cta_path.typeApp handles by…
open_checkoutOpening Stripe Checkout for the specified plan
view_plansNavigating to plans page (or chained placement)
book_demoOpening booking tool
contact_salesOpening sales contact form
complete_onboardingNavigating to onboarding task
invite_teammateOpening invite flow
open_rt_placementEvaluating a chained placement via placementHandle
customApp-defined action via handle
dismissClosing the placement
snoozeClosing and re-queuing for later
function handleCTA(placement) {
const { cta_path } = placement;
switch (cta_path.type) {
case "open_checkout":
return openCheckout(cta_path.plan_handle, cta_path.promotion_id);
case "view_plans":
if (cta_path.placement_handle) {
const p = rt.getPlacement({
placementHandle: cta_path.placement_handle
});
return p ? renderPlacement(p) : navigateToPlans();
}
return navigateToPlans(cta_path.plan_handle);
case "open_rt_placement":
const next = rt.getPlacement({
placementHandle: cta_path.placement_handle
});
return next ? renderPlacement(next) : navigateToPlans();
case "book_demo":
return openBooking();
case "contact_sales":
return openSalesContact();
}
}
{
"output_id": "gated_ai_export__free_users",
"category": "gated_feature",
"surface": {
"template": "modal_overlay_optional",
"type": "modal",
"slot_id": "feature_gate_modal",
"entitlement_handle": "ai_export"
},
"content": {
"header": "Unlock AI Export",
"body": "Upgrade to Pro for AI enhancements.",
"cta_label": "Upgrade to Pro"
},
"promotion": { "id": "promo_20_off_annual", "discount": "20%" },
"cta_path": {
"type": "open_checkout",
"plan_handle": "pro",
"promotion_id": "promo_20_off_annual"
},
"decision_id": "dec_abc123",
"config_version": "v42",
"present_upsell": true
}

Standard output fields: config_version (string) lets the app detect stale config. decision_id (string) is a unique ID for debugging and analytics. present_upsell (boolean) indicates whether the placement includes an upgrade or expansion offer.

Modal (surfaceType: "modal") is interruptive — it takes over the screen. Only request a modal at safe moments: when the user has just performed an action, completed a task, or reached a natural transition. Never on passive page render.

Non-interruptive types (banner, in_page, button, toast) are safe on render.