Integration
Stripe revenue attribution
This is the integration the product is built around. Two webhook events and one field on your Checkout session turn a traffic dashboard into a revenue dashboard — and because the amounts arrive signed from Stripe rather than from a browser, the numbers reconcile with your Stripe balance instead of approximating it.
Add the webhook endpoint in Stripe
In the Stripe dashboard, add an endpoint pointing at Revtrail and subscribe it to checkout.session.completed and invoice.payment_succeeded. The first covers the initial purchase; the second covers every renewal after it, which is what makes lifetime value by channel possible.
Stripe Dashboard → Developers → Webhookshttps://revtrail.pyln.dev/api/fn/stripeRevenue Events: checkout.session.completed invoice.payment_succeededSave the signing secret in your site settings
Stripe shows a whsec_… signing secret when you create the endpoint. Paste it into the site's settings in Revtrail. Every incoming webhook is verified against it server-side, so nobody can post fake revenue at your dashboard by guessing the URL.
Pass the visitor id into Checkout
This is the line that joins a payment to a journey. Read the visitor id with the async accessor and set it as the Checkout session's client_reference_id, or as metadata.revtrail_visitor_id if you're already using client_reference_id for something else.
server-side, where you create the session// client: read the id once the first beacon has answered const visitorId = await revtrail.visitorIdAsync(); // server: attach it to the session await stripe.checkout.sessions.create({ client_reference_id: visitorId ?? undefined, mode: "subscription", line_items: [{ price: PRICE_ID, quantity: 1 }], success_url: "https://example.com/welcome", });Check a real payment end to end
Run one live payment (or a test-mode one against a test webhook) and watch it appear against a source in the dashboard. If it lands as 'direct' when it shouldn't, the visitor id didn't make it into the session — that's the failure mode worth catching before you rely on the numbers.
Why revenue can't come from the browser
The public site key is embedded in your HTML, which means anyone can read it and post events with it. That's fine for pageviews and conversion counts, and completely unacceptable for financial data. The beacon rejects revenueCents, currency, and customerId outright: the only path for money into your dashboard is a signature-verified webhook from a payment provider. It's a constraint that costs a little flexibility and buys a number you can quote to an investor.
Renewals keep the original credit
The first payment maps a Stripe customer to a visitor journey. Every invoice.payment_succeeded after that reuses the mapping, so an annual renewal eleven months later still credits the blog post that brought the customer. That's what separates 'this campaign produced $200 of first payments' from 'this campaign produced customers worth $2,400 so far', and only the second number should decide a budget.
Use the async visitor id, not the sync one
revtrail.visitorId() returns null until the first beacon has come back from the server. On a fast checkout page — a deep link straight to a buy button — that race is lost more often than won, and the payment arrives unattributed. await revtrail.visitorIdAsync() waits for the id. Treat it as best-effort either way: pass undefined when it's null, and never let an analytics call block or fail a checkout.
Duplicate webhooks are handled
Stripe retries deliveries, and a retried delivery must not double your revenue. Every webhook event ID is recorded and deduplicated, so a retry is a no-op. You don't need to build idempotency on your side for this integration.
What about Paddle, LemonSqueezy, or Chargebee?
There's no built-in integration for them today, and we'd rather say that here than sell you a page that implies otherwise. Stripe and RevenueCat are the two verified revenue paths that exist. If you're on another processor you can still track conversion events (revtrail('purchase')) for counts and funnels — you just won't get verified amounts until we build it. Tell us which one you need; that's how it gets prioritised.
Questions people actually ask
- Does this work with Stripe Payment Links?
- Partly. Payment Links complete without your code creating the session, so there's no place to attach the visitor id and the payment attributes as direct. If attribution matters for a flow, create the Checkout session yourself.
- What happens to refunds?
- The integration listens for checkout.session.completed and invoice.payment_succeeded. Refunds aren't subtracted automatically today, so a heavily refunded channel will look better than it is. Check your Stripe reports for net revenue.
- Can I connect one Stripe account to several sites?
- Each site holds its own webhook signing secret, so you point a webhook per site. A single Stripe account can feed several sites that way.
- Is my Stripe signing secret stored safely?
- It's stored encrypted at rest and is never returned to the browser — site settings show a placeholder rather than the value.
Integrations