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 monetization policy — which plans exist, what each entitles, and when a placement shows. The customer’s app owns enforcement: it reads the entitlement result and acts on it, with no hard-coded plan logic. Plan changes, entitlement updates, and placement configurations take effect immediately without code deploys. For billing-critical or abuse-sensitive actions, re-check entitlement on your server — the client check is a UX convenience. See Client vs Server Enforcement.

MethodPurposeOutputs
initRevTurbine(config)Initialize the 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.can(handle, context?)Pure access check — feature, usage, credits, seats, tiers.{ status, currentTier?, reason? }
rt.update(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.track(name, data?)Behavioral event for targeting and analytics.
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 = await rt.can("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 = await rt.can("ai_credits", { used });
// Branch on `allowed` — it is the verdict. `status` is the shape of the
// answer, and `limited` appears on both sides of it.
if (access.allowed) {
executeAction();
billing.recordUsage("ai_credits", 1);
// Usage goes under `usage`. A bare handle is not a usage report.
rt.update({ usage: { ai_credits: billing.getUsed("ai_credits") } });
// `limited` AND allowed means at or past the limit under an enforcement
// that lets it through — `degrade` or `allow_overage`. Nudge, don't block.
if (access.status === "limited") {
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.