Billing & Stripe
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
Section titled “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.
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
Section titled “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
Section titled “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.
// Your checkout, when it creates the Stripe customerconst 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
Section titled “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 thepayment_failedretention qualifier server-side (and clear again oninvoice.payment_succeeded).- Account/person (v2) events → connected-account status shown in Settings.
Verify in test mode
Section titled “Verify in test mode”Walk this once after connecting, before anything real depends on the pipeline:
-
Fire synthetic events on your connected account. With the Stripe CLI, in test mode:
Terminal window stripe trigger customer.subscription.created --stripe-account acct_…stripe trigger invoice.payment_failed --stripe-account acct_… -
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). -
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.
-
Confirm recovery.
stripe trigger invoice.payment_succeededshould clear the billing-health issue again.
Integration settings
Section titled “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
Section titled “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
Section titled “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:
// 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:
// 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 },});metadataon the session is whatcheckout.session.completedcarries.subscription_data.metadatamirrors the bag onto the Subscription, so everycustomer.subscription.*event carries it and every invoice snapshots it underparent.subscription_details.metadata— renewals, failed payments and recoveries stay attributed to the placement without a customer mapping.client_reference_idis 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.